Recent

Author Topic: Pidgin plugin  (Read 9901 times)

Dibo

  • Hero Member
  • *****
  • Posts: 1048
Pidgin plugin
« on: October 15, 2010, 12:35:22 pm »
Hi,

I wonder if anyone has experience writing plugins for Pidgin IM using Lazarus? Some time ago I wrote some plugins for some IM on Windows. Then I used simple DLLs. But pidgin API looks more complicated. I found this site:
http://developer.pidgin.im/wiki/
In "Plugins and scripting" section are some examples. I think "C plugin HOWTO" is restricted only for C language (from what I read, this example uses pidgin and libpurple headers). There is example with D-Bus too, but I read negative opinions about D-Bus on linux. I see, that FPC have support for D-Bus:
http://wiki.freepascal.org/FPC_and_DBus

In conclusion, is there a way to write a simple .SO plugin for pidgin?

Regards.

Dibo

  • Hero Member
  • *****
  • Posts: 1048
Re: Pidgin plugin
« Reply #1 on: October 15, 2010, 01:27:52 pm »
Hm, this python example work fine:

Code: Python  [Select][+][-]
  1. #!/usr/bin/env python
  2.  
  3. def my_func(account, sender, message, conversation, flags):
  4.     print sender, "said:", message
  5.  
  6. import dbus, gobject
  7. from dbus.mainloop.glib import DBusGMainLoop
  8. dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
  9. bus = dbus.SessionBus()
  10.  
  11. bus.add_signal_receiver(my_func,
  12.                         dbus_interface="im.pidgin.purple.PurpleInterface",
  13.                         signal_name="ReceivedImMsg")
  14.  
  15. loop = gobject.MainLoop()
  16. loop.run()
  17.  

But I can't find FPC alternative for "bus.add_signal_receiver". On wiki page, "Receiving a Signal" section is empty. There is no documentation or signals are not implemented yet?

Dibo

  • Hero Member
  • *****
  • Posts: 1048
Re: Pidgin plugin
« Reply #2 on: October 15, 2010, 04:59:23 pm »
Ok, I find some C example and rewrite it to FPC. This is simple example how to listening pidgin messages using D-Bus. Maybe someone need it. I wrote it for my quick test so there may be some mistakes and errors. I used thread instead of glib loop. D-Bus works pretty good, but I'm still curious whether it is possible to write a .SO library with libpurple headers like in "C plugin HOWTO" example.

Pidgin plugin (D-Bus)
Code: Pascal  [Select][+][-]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  9.   ExtCtrls, dbus;
  10.  
  11. type
  12.  
  13.   TListeningThread = class;
  14.  
  15.   { TForm1 }
  16.  
  17.   TForm1 = class(TForm)
  18.     Button1: TButton;
  19.     Memo1: TMemo;
  20.     Panel1: TPanel;
  21.     procedure Button1Click(Sender: TObject);
  22.     procedure FormCreate(Sender: TObject);
  23.     procedure FormDestroy(Sender: TObject);
  24.   private
  25.     { private declarations }
  26.     th: TListeningThread;
  27.   public
  28.     { public declarations }
  29.   end;
  30.  
  31.   { TTest }
  32.  
  33.   TListeningThread = class(TThread)
  34.   private
  35.     conn: PDBusConnection;
  36.     FLog: String;
  37.     procedure SynchAddLog;
  38.     procedure AddLog(const S: String);
  39.   protected
  40.     procedure Execute; override;
  41.   public
  42.   end;
  43.  
  44. var
  45.   Form1: TForm1;
  46.  
  47. implementation
  48.  
  49. uses ctypes;
  50.  
  51. {$R *.lfm}
  52.  
  53. { TForm1 }
  54.  
  55. procedure TForm1.FormCreate(Sender: TObject);
  56. begin
  57.   th := nil
  58. end;
  59.  
  60. procedure TForm1.Button1Click(Sender: TObject);
  61. begin
  62.   // Start listening
  63.   th := TListeningThread.Create(False);
  64. end;
  65.  
  66. procedure TForm1.FormDestroy(Sender: TObject);
  67. begin
  68.  
  69.   // Terminating thread
  70.   if th<>nil then
  71.   begin
  72.     th.Terminate;
  73.     th.WaitFor;
  74.     th.Free;
  75.   end;
  76. end;
  77.  
  78. { TTest }
  79.  
  80. procedure TListeningThread.SynchAddLog;
  81. begin
  82.   Form1.Memo1.Lines.Add(FLog);
  83. end;
  84.  
  85. procedure TListeningThread.AddLog(const S: String);
  86. begin
  87.   FLog := s;
  88.   Synchronize(@SynchAddLog);
  89. end;
  90.  
  91. procedure TListeningThread.Execute;
  92. var
  93.   err: DBusError;
  94.   ret: cint;
  95.   msg: PDBusMessage;
  96.   args: DBusMessageIter;
  97.   id: cint;
  98.   sender, body: Pchar;
  99. begin
  100.   { Initializes the errors }
  101.   dbus_error_init(@err);
  102.  
  103.   { Connection }
  104.   conn := dbus_bus_get(DBUS_BUS_SESSION, @err);
  105.   try
  106.  
  107.     if dbus_error_is_set(@err) <> 0 then
  108.     begin
  109.       AddLog('Connection Error: ' + err.message);
  110.       dbus_error_free(@err);
  111.     end;
  112.  
  113.     if conn = nil then Exit;
  114.  
  115.     { Request the name of the bus }
  116.     ret := dbus_bus_request_name(conn, 'im.pidgin.purple.PurpleInterface', DBUS_NAME_FLAG_REPLACE_EXISTING, @err);
  117.  
  118.     if dbus_error_is_set(@err) <> 0 then
  119.     begin
  120.       AddLog('Name Error: ' + err.message);
  121.       dbus_error_free(@err);
  122.     end;
  123.  
  124.     if ret <> DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER then Exit;
  125.  
  126.     dbus_bus_add_match(conn, 'type=signal, interface=im.pidgin.purple.PurpleInterface', @err);
  127.     dbus_connection_flush(conn);
  128.  
  129.     if dbus_error_is_set(@err) <> 0 then
  130.     begin
  131.       AddLog('Match Error: ' + err.message);
  132.       dbus_error_free(@err);
  133.       Exit
  134.     end;
  135.  
  136.     while not Terminated do begin
  137.       dbus_connection_read_write(conn, 0);
  138.       msg := dbus_connection_pop_message(conn);
  139.  
  140.       if msg=nil then begin
  141.         sleep(300);
  142.         Continue;
  143.       end;
  144.  
  145.       if (dbus_message_is_signal(msg, 'im.pidgin.purple.PurpleInterface', 'ReceivedImMsg')=1) then
  146.       begin
  147.         // read the parameters
  148.         if (dbus_message_iter_init(msg, @args)=0) then
  149.           AddLog('Message has no arguments!')
  150.         else begin
  151.           dbus_message_get_args(msg, @err, DBUS_TYPE_INT32, [@id, DBUS_TYPE_STRING, @sender, DBUS_TYPE_STRING, @body, DBUS_TYPE_INVALID]);
  152.           AddLog(Format('New messsage from: %s; Message: %s', [sender, body]));
  153.         end
  154.       end;
  155.  
  156.       // free the message
  157.       dbus_message_unref(msg);
  158.  
  159.     end;
  160.   finally
  161.     dbus_connection_close(conn);
  162.   end;
  163. end;
  164.  
  165. end.
  166.  
« Last Edit: October 15, 2010, 05:46:55 pm by Dibo »

prof7bit

  • Full Member
  • ***
  • Posts: 161
Re: Pidgin plugin
« Reply #3 on: April 22, 2012, 04:06:09 pm »
Just for the record (for the search engines since this topic comes up as the first result when searching for free pascal pidgin plugin) I have ported the minimal example found here:

http://developer.pidgin.im/wiki/CHowTo/BasicPluginHowto

to Free Pascal. Its actually much easier than I initially thought, after spending some time digging through the various headers and understanding the mechanism behind the PURPLE_INIT_PLUGIN macro and the global info variable I have successfully ported the example to FPC.

My exmple consists of a library testplugin which will implement all the functionality and is using the unit purple which defines the stuff found in the various libpurple headers. Here is the unit purple:

Code: [Select]
{ purple.pas

  A minimalistic header translation for making libpurple plugins.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
}

unit purple;

{$mode objfpc}{$H+}

interface

const
  LIBPURPLE = 'libpurple';

  PURPLE_PLUGIN_MAGIC = 5;
  PURPLE_MAJOR_VERSION = 2;
  PURPLE_MINOR_VERSION = 10;
  PURPLE_PRIORITY_DEFAULT = 0;


type
  PGList = ^TGList;
  TGList = packed record
    data: Pointer;
    next: PGlist;
    prev: PGList;
  end;

  GBoolean = LongBool; {$warning FIXME, need int sized bool!}

  TPurplePluginType = (
    PURPLE_PLUGIN_UNKNOWN  := -1,  // Unknown type.
    PURPLE_PLUGIN_STANDARD := 0,   // Standard plugin.
    PURPLE_PLUGIN_LOADER,          // Loader plugin.
    PURPLE_PLUGIN_PROTOCOL         // Protocol plugin.
  );

  TPurplePluginPriority = Integer;
  PPurplePluginUiInfo= Pointer; {$warnig define me!}

  PPurplePluginInfo = ^TPurplePluginInfo;

  PPurplePlugin = ^TPurplePlugin;
  TPurplePlugin = packed record
    native_plugin: GBoolean;       // Native C plugin.
    loaded: GBoolean;              // The loaded state.
    handle: Pointer;               // The module handle.
    path: PChar;                   // The path to the plugin.
    info: PPurplePluginInfo;       // The plugin information.
    error: PChar;

    ipc_data: Pointer;             // IPC data.
    extra: Pointer;                // Plugin-specific data.
    unloadable: GBoolean;          // Unloadable
    dependent_plugins: PGList;     // Plugins depending on this

    _purple_reserved1: Pointer;
    _purple_reserved2: Pointer;
    _purple_reserved3: Pointer;
    _purple_reserved4: Pointer;
  end;

  TPurplePluginInfo = packed record
    magic: Integer;
    major_version: Integer;
    minor_version: Integer;
    plugintype: TPurplePluginType;
    ui_requirement: PChar;
    flags: LongInt;
    dependencies: PGList;
    priority: TPurplePluginPriority;

    id: PChar;
    name: PChar;
    version: PChar;
    summary: PChar;
    description: PChar;
    author: PChar;
    homepage: PChar;

    { If a plugin defines a 'load' function, and it returns FALSE,
      then the plugin will not be loaded.}
    load: function(plugin: PPurplePlugin): GBoolean; cdecl;
    unload: function(plugin: PPurplePlugin): GBoolean; cdecl;
    destroy: procedure(plugin: PPurplePlugin); cdecl;

    ui_info: Pointer;
    extra_info: Pointer;

    { Used by any plugin to display preferences.
      If #ui_info has been specified, this will be ignored.}
    prefs_info: PPurplePluginUiInfo;

    { This callback has a different use depending on whether this
      plugin type is PURPLE_PLUGIN_STANDARD or PURPLE_PLUGIN_PROTOCOL.

      If PURPLE_PLUGIN_STANDARD then the list of actions will show up
      in the Tools menu, under a submenu with the name of the plugin.
      context will be NULL.

      If PURPLE_PLUGIN_PROTOCOL then the list of actions will show up
      in the Accounts menu, under a submenu with the name of the
      account.  context will be set to the PurpleConnection for that
      account.  This callback will only be called for online accounts.}
    actions: function(plugin: PPurplePlugin; context: Pointer): PGList; cdecl;

    _purple_reserved1: Pointer;
    _purple_reserved2: Pointer;
    _purple_reserved3: Pointer;
    _purple_reserved4: Pointer;
  end;

var
  PluginInfo: TPurplePluginInfo = ();


function purple_plugin_register(Plugin: PPurplePlugin): GBoolean; cdecl; external LIBPURPLE;
function purple_init_plugin(Plugin: PPurplePlugin): GBoolean; cdecl;

implementation

{ This re-implements the stuff that is behind the PURPLE_INIT_PLUGIN macro.
  In C the macro would define the function and export it, here we only
  define it and the library must export it (because we can't export it
  from within a unit directly). }
function purple_init_plugin(Plugin: PPurplePlugin): GBoolean; cdecl;
begin
  Plugin^.info := @PluginInfo;
  Result := purple_plugin_register(Plugin);
end;

begin
end.

and here is my hello world plugin using this unit:

Code: [Select]
library testplugin;

{$mode objfpc}{$H+}

uses
  purple;


function OnLoad(Plugin: PPurplePlugin): GBoolean; cdecl;
begin
  writeln('purple has called our load callback :-)');
  Result := True;
end;

function OnUnload(Plugin: PPurplePlugin): GBoolean; cdecl;
begin
  writeln('purple has called our unload callback :-)');
  Result := True;
end;

exports
  purple_init_plugin;

begin
  with PluginInfo do begin
    magic := PURPLE_PLUGIN_MAGIC;
    major_version := PURPLE_MAJOR_VERSION;
    minor_version := PURPLE_MINOR_VERSION;
    plugintype := PURPLE_PLUGIN_STANDARD;
    priority := PURPLE_PRIORITY_DEFAULT;
    id := 'core-hello_world';
    name := 'Hello World';
    version := '1.0';
    summary := 'Hello World Plugin';
    description := 'This is a Hello-World Plugin written in Free Pascal';
    author := 'Bernd Kreuss';
    homepage := 'http://example.com/';
    load := @OnLoad;
    unload := @OnUnload;
  end;
end.

This code is of course totally incomplete but it serves as an explanation how the plugin interface is supposed to work. In the official C example I linked at the beginning they are defining this ominous info variable and then they have this even more obscure macro PURPLE_INIT_PLUGIN at the very end of the file where no code is executed at all.

The structure of the info variable which they initialize in this horribly sloppy way is actually represented as a struct in the C headers and I properly defined it in TPurplePluginInfo and initialize it at the beginning when the library is loaded.

The macro PURPLE_INIT_PLUGIN actually expands to a complete function definition and export of this function. I am defining this function in purple.pas and export it from within the main unit.


The following is what is happening when Pidgin loads a plugin:

When Pidgin is started and libpurple loaded it will scan the plugin folder for all .so (or .dll) files and will probe each of them. Probing works like this:

It will load the library
It will check whether there is a function purple_init_plugin()
It will call the function purple_init_plugin()

If any of the above fails then it is not a purple plugin and it will immediately unload the library again.

When our library is loaded then the RTL will immediately execute the part between begin and end *before* anything else happens. This is where we initialize our info record.

Then shortly after this libpurple will call our exported purple_init_plugin() where we give it the pointer to our info variable and call the register function, now libpurple can read everything it needs to know about our plugin from this record.

The plugin will show up in the list of plugins and if we activate it then it will call our OnLoad() function and if we deactivate it it will call the OnUnload() function.

The above code is of course totally incomplete, there are a lot more external purple functions and type definitions that need to go into purple.pas to make it do anything useful but this is the first version after a few hours of experimenting that actually worked, it is a cleaned up snapshot of the code at the exact moment when I exclaimed "Eureka!". It can serve as a starting point for further experimentation.

___
I imported the entire Pidgin source code as a project into Eclipse CDT to be able to easily navigate and follow the definitions in the various header files. The pidgin sources also contain a "null protocol" plugin which is the hello world equivalent of a protocol plugin and meant as an example for implementing a protocol, this will be the next step I am going to investigate.

Dibo

  • Hero Member
  • *****
  • Posts: 1048
Re: Pidgin plugin
« Reply #4 on: April 23, 2012, 05:04:48 pm »
Wow, thanks!
I have tried translate pidgin C headers to pascal but I don't have any experience in C. I used tools like C2pas (or something like that) but with no success. Finally I created plugin using DBus API ;)
Now I can try rewrite it to native library. Thanks.

prof7bit

  • Full Member
  • ***
  • Posts: 161
Re: Pidgin plugin
« Reply #5 on: April 25, 2012, 01:19:02 pm »
You can follow my development here:
https://github.com/prof7bit/TorChat/tree/torchat2/src/purple

Its still in early stages, it will become a protocol plugin. I'm still experimenting what is the best way to translate and/or wrap some of the pidgin API and how do design the actual protocol implementation and I will add individual definitions incrementally as I need them. I have not much (and not always) time to put into it, so it is growing only slowly and you will observe months without any activity but once it is finished it could serve as an example plugin. I'm trying to design purple.pas in such a way that it could be used for many other plugins too, there will be no TorChat-specific stuff in it.

This git repository has 2 branches, master (the default branch) is the current prototype implementation of the protocol and gui client written in Python, the branch torchat2 will be a complete and modular rewrite from the ground up in ObjectPascal. I am intentionally starting with the pidgin plugin first and only if this is completed I will make a standalone LCL-GUI application for it using the same core protocol units.

The protocol units (still incomplete) are in the "client" folder, the pidgin plugin in the "purple" folder.

 

TinyPortal © 2005-2018