pax_global_header00006660000000000000000000000064134320675620014522gustar00rootroot0000000000000052 comment=4cf4512aa6a439e2e9a21f9f60c237b3d936693b bitlbee-mastodon-1.4.1/000077500000000000000000000000001343206756200147555ustar00rootroot00000000000000bitlbee-mastodon-1.4.1/.gitignore000066400000000000000000000003131343206756200167420ustar00rootroot00000000000000*~ *.la *.lo *.o *.tar.* .deps .libs aclocal.m4 autom4te.cache build-aux config.log config.status configure debian INSTALL libtool libtool.m4 lt*.m4 Makefile Makefile.in config.h* stamp-h1 TAGS src/TAGS bitlbee-mastodon-1.4.1/HISTORY.md000066400000000000000000000027531343206756200164470ustar00rootroot00000000000000# The History of this Plugin New features for 1.4.0: - new `filter` command - new `list` command - new settings to hide notifications by type Incompatible change in 1.4.0: If you have subscribed to a hashtag, you need to change your channel settings and prepend the hash. Without the hash, the plugin gets confused and things the channel is for a list of the same name. Do this from the control channel (&bitlbee). Let's assume you have a channel called #hashtag. It's *room* setting should be #hashtag. If it's lacking the initial hash: > **<kensanata>** channel #hashtag set room > **<root>** room = `hashtag' > **<kensanata>** channel #hashtag set room #hashtag > **<root>** room = `#hashtag' There, fixed it. New features for 1.3.1: - new `visibility` command - new `cw` command - removed support for posting a content warning using CW1 New features for 1.2.0: - format search results - new `bio` command - new `pinned` command - add all the accounts when replying - fixed list of accounts in the channel when connecting New features for 1.1.0: - new `hide_sensitive` setting # Beginnings This plugin started out as a fork of Bitlbee itself with the Mastodon code being based on a copy of the Twitter code. When it became clear that my code just wasn't going to get merged, I took a look at the Facebook and Discord plugins for Bitlbee and decided that it should be easy to turn my existing code into a plugin. Luckily, that worked as intended. – Alex Schroeder bitlbee-mastodon-1.4.1/LICENSE000066400000000000000000000432541343206756200157720ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. bitlbee-mastodon-1.4.1/Makefile.am000066400000000000000000000013701343206756200170120ustar00rootroot00000000000000# Copyright 2016 Artem Savkov # # 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, see . AUTOMAKE_OPTIONS = foreign ACLOCAL_AMFLAGS = -Im4 SUBDIRS = src doc bitlbee-mastodon-1.4.1/README.md000066400000000000000000000153361343206756200162440ustar00rootroot00000000000000Mastodon plugin for Bitlbee =========================== This plugin allows [Bitlbee](https://www.bitlbee.org/) to communicate with [Mastodon](https://joinmastodon.org/) instances. Mastodon is a free, open-source, decentralized microblogging network. Bitlbee is an IRC server connecting to various other text messaging services. You run Bitlbee and connect to it using an IRC client, then configure Bitlbee to connect to other services, such as a Mastodon instance where you already have an account. The benefit is that you can now use [any IRC client](https://en.wikipedia.org/wiki/Comparison_of_Internet_Relay_Chat_clients) you want to connect to Mastodon. Please report issues using the [Software Wiki](https://alexschroeder.ch/software/Bitlbee_Mastodon). For questions, ping **kensanata** on `irc.oftc.net/#bitlbee`. **Table of Contents** - [License](#license) - [Usage](#usage) - [Build dependencies](#build-dependencies) - [Building and Installing](#building-and-installing) - [Debugging](#debugging) License ------- Most of the source code is distributed under the [GNU Lesser Public License 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html#SEC1). The build system is distributed under the [GNU Public License 2.0 or any later version](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html#SEC1). Anything without an obvious license in the file header also uses the GPL 2.0 or any later version. Usage ----- Please refer to the Bitlbee help system: ``` > help mastodon ``` Alternatively, a snapshot of the entries added to the help system by this plugin are available on the [help page](https://alexschroeder.ch/cgit/bitlbee-mastodon/tree/doc/HELP.md#top). Build dependencies ------------------ - `bitlbee` and headers >= 3.5 If you haven't built Bitlbee yourself you will need to install the dev package, usually `bitlbee-dev` or `bitlbee-devel`. If Bitlbee was built from source don't forget to do `make install-dev`. To NetBSD users: your Bitlbee doesn't include the devel files. One way to fix this is to build Bitlbee via `pkgsrc`. You'll need to add to the `chat/bitlbee` pkgsrc `Makefile`, in the `post-build` hook, this line: ``` cd ${WRKSRC} && ${GMAKE} DESTDIR=${DESTDIR} install-dev ``` Don't forget to regenerate your `PLIST` (`plugindir` is `/usr/pkg/lib/bitlbee`) with: ``` make print-PLIST > PLIST ``` - `glib2` and headers => 2.32 The library itself is usually installed as a dependency of Bitlbee but headers need to be installed separately. In Debian, the package containing them is `libglib2.0-dev`. - `autotools` (if building from git) A bit of an overkill, but it works. If you don't have this package, try looking for `autoconf` and `libtool`. \*BSD users should install `autoconf`, `automake` and `libtool`, preferably the latest version available. FreeBSD will also need `pkgconfig` on top of that. Building and Installing ----------------------- Check your distribution: - FreeBSD: `irc/bitlbee-mastodon` Alternatively, build it from source. You need to generate the autotools configuration script and related files by executing the following command: ``` ./autogen.sh ``` After that, you can build as usual: ``` ./configure make sudo make install ``` 🔥 If your Bitlbee's plugindir is in a non-standard location you need to specify it: `./configure with --with-plugindir=/path/to/plugindir` 🔥 If you're installing this plugin in a system where you didn't build your own Bitlbee but installed revision 3.5.1 (e.g. on a Debian system around the end of 2017), you will run into a problem: the plugin will get installed into `/usr/lib/bitlbee` (`plugindir`) but the documentation wants to install into `/usr/local/share/bitlbee` instead of `/usr/share/bitlbee` (`datadir`). As you can tell from `/usr/lib/pkgconfig/bitlbee.pc`, there is no `datadir` for you. In this situation, try `./configure --prefix=/usr` and build and install again. Debugging --------- Before debugging Bitlbee, you probably need to stop the system from running Bitlbee. I'm still unsure of how to do it. ``` sudo killall bitlbee ``` Usually my system will restart Bitlbee after a bit, though. So I'll try some of the following: ``` sudo systemctl stop bitlbee ``` You can enable extra debug output for `bitlbee-mastodon` by setting the `BITLBEE_DEBUG` environment variable. This will print all traffic it exchanges with Mastodon servers to STDOUT and there is a lot of it. To get it on your screen run `bitlbee` in foreground mode: ``` BITLBEE_DEBUG=1 bitlbee -nvD ``` If you need to read your config file from the standard location: ``` BITLBEE_DEBUG=1 sudo -u bitlbee bitlbee -nvD ``` If you need to use a debugger, make a copy of `/etc/bitlbee/bitlbee.conf` (or simply create an empty file) and `/var/lib/bitlbee/`. Then run `gdb`, set the breakpoints you want (answer yes to "Make breakpoint pending on future shared library load?"), and run it using the options shown: ``` touch bitlbee.conf sudo cp /var/lib/bitlbee/*.xml . gdb bitlbee b mastodon_post_message y run -nvD -c bitlbee.conf -d . ``` Then connect with an IRC client as you usually do. If you're getting error messages about the address being in use, you haven't managed to kill the existing Bitlbee. ``` Error: bind: Address already in use ``` Check who's listening on port 6667: ``` sudo lsof -i:6667 ``` Then do what is necessary to kill it. 😈 Note that perhaps you must remove the `-O2` from `CFLAGS` in the `src/Makefile` and run `make clean && make && sudo make install` in the `src` directory in order to build and install the module without any compiler optimisation. If you run `make` in the top directory, `src/Makefile` will get regenerated and you'll get your optimized code again. You know you're running optimized code when things seem to repeat themselves in strange ways: ``` (gdb) n 594 if (!mastodon_length_check(ic, message)) { (gdb) 583 { (gdb) 594 if (!mastodon_length_check(ic, message)) { (gdb) 584 struct mastodon_data *md = ic->proto_data; (gdb) 594 if (!mastodon_length_check(ic, message)) { (gdb) 584 struct mastodon_data *md = ic->proto_data; (gdb) 594 if (!mastodon_length_check(ic, message)) { (gdb) ``` Or when values can't be printed: ``` (gdb) p m->str value has been optimized out ``` WARNING: there *is* sensitive information in this debug output, such as auth tokens, your plaintext password and, obviously, your incoming and outgoing messages. Be sure to remove any information you are not willing to share before posting it anywhere. If you are experiencing crashes please refer to [debugging crashes](https://wiki.bitlbee.org/DebuggingCrashes) for information on how to get a meaningful backtrace. bitlbee-mastodon-1.4.1/RELEASE.md000066400000000000000000000003521343206756200163570ustar00rootroot00000000000000# How to prepare a release 1. verify that `mnews` in `mastodon-help.txt` is up to date 2. change the version number in `configure.ac` 3. update `HISTORY.md` 4. commit it all 5. tag the commit 6. push the tag to `origin` and `github` bitlbee-mastodon-1.4.1/autogen.sh000077500000000000000000000013611343206756200167570ustar00rootroot00000000000000#!/bin/sh # Copyright 2016 Artem Savkov # # 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, see . mkdir -p m4 autoreconf --verbose --force --install bitlbee-mastodon-1.4.1/configure.ac000066400000000000000000000050151343206756200172440ustar00rootroot00000000000000# Copyright 2016 Artem Savkov # Copyright 2017-2018 Alex Schroeder # # 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, see . AC_INIT( [bitlbee-mastodon], [1.4.0], [https://github.com/kensanata/bitlbee-mastodon/issues], [bitlbee-mastodon], [https://alexschroeder.ch/cgit/bitlbee-mastodon/about/], ) AC_CONFIG_AUX_DIR([build-aux]) AC_CONFIG_MACRO_DIR([m4]) AM_INIT_AUTOMAKE([no-define]) AC_PROG_CC AM_PROG_CC_C_O AC_DISABLE_STATIC AC_PROG_LIBTOOL m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) m4_ifdef([AC_PROG_CC_C99], [AC_PROG_CC_C99]) # Define PKG_CHECK_VAR() for pkg-config < 0.28 m4_define_default( [PKG_CHECK_VAR], [AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config]) AS_IF([test -z "$$1"], [$1=`$PKG_CONFIG --variable="$3" "$2"`]) AS_IF([test -n "$$1"], [$4], [$5])] ) # Checks for libraries. PKG_CHECK_MODULES([BITLBEE], [bitlbee >= 3.5]) PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.32]) AC_CONFIG_HEADERS([config.h]) # Checks for typedefs, structures, and compiler characteristics. AC_TYPE_SIZE_T # Checks for library functions. AC_CHECK_FUNCS([memset]) # bitlbee-specific stuff AC_ARG_WITH([plugindir], [AS_HELP_STRING([--with-plugindir], [BitlBee plugin directory])], [plugindir="$with_plugindir"] ) AS_IF( [test -z "$plugindir"], [PKG_CHECK_VAR( [BITLBEE_PLUGINDIR], [bitlbee], [plugindir], [plugindir="$BITLBEE_PLUGINDIR"], [plugindir="$libdir/bitlbee"] )] ) AC_SUBST([plugindir]) AC_ARG_WITH([bdatadir], [AS_HELP_STRING([--with-bdatadir], [BitlBee data directory])], [bdatadir="$with_bdatadir"] ) AS_IF( [test -z "$bdatadir"], [PKG_CHECK_VAR( [BITLBEE_DATADIR], [bitlbee], [datadir], [datadir="$BITLBEE_DATADIR"], [datadir="$datarootdir/bitlbee"] )], [datadir="$bdatadir"] ) AC_SUBST([datadir]) AC_CONFIG_FILES([Makefile src/Makefile doc/Makefile]) AC_OUTPUT bitlbee-mastodon-1.4.1/doc/000077500000000000000000000000001343206756200155225ustar00rootroot00000000000000bitlbee-mastodon-1.4.1/doc/HELP.md000066400000000000000000000634111343206756200166010ustar00rootroot00000000000000# Bitlbee Mastodon This document was generated from the help text for the plugin. Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly. * *[news](#news)* - Backwards incompatible changes * *[register](#register)* - Registering an account * *[connect](#connect)* - Connecting to an instance * *[read](#read)* - Reading your timeline * *[post](#post)* - Posting a new status * *[undo](#undo)* - Undo and redo * *[context](#context)* - Showing a status in its context * *[reply](#reply)* - Replying to a status * *[delete](#delete)* - Deleting a status * *[favourite](#favourite)* - Favouring a status * *[follow](#follow)* - Following an account * *[block](#block)* - Blocking an account * *[mute](#mute)* - Muting an account * *[boost](#boost)* - Boosting a status * *[more](#more)* - Getting more information about things * *[search](#search)* - Searching for accounts and hashtags * *[spam](#spam)* - Reporting a status * *[control](#control)* - Commands in the control channel * *[hashtag](#hashtag)* - Showing and following a hashtag * *[public](#public)* - Showing and following the local or federated timeline * *[lists](#lists)* - Managing lists * *[filters](#filters)* - Managing filters * *[notifications](#notifications)* - Showing your notifications * *[set](#set)* - Settings affecting Mastodon accounts ## news Use **plugins** in the control channel (**&bitlbee**) to learn which version of the plugin you have. Incompatible change in **1.4.0**: If you have subscribed to a hashtag, you need to change your channel settings and prepend the hash. Do this from the control channel (**&bitlbee**). Let's assume you have a channel called #hashtag. It's **room** setting should be **#hashtag**. If it's lacking the initial hash: > **<kensanata>** channel #hashtag set room > **<root>** room = `hashtag' > **<kensanata>** channel #hashtag set room #hashtag > **<root>** room = `#hashtag' Don't forget to save your settings. ## register You need to register your Mastodon account on an **instance**. See https://instances.social/ if you need help picking an instance. It's a bit like picking a mail server and signing up. Sadly, there is currently no way to do this from IRC. Your need to use a web browser to do it. Once you have the account, see **help account add mastodon** for setting up your account. ## set These settings will affect Mastodon accounts: * **set auto_reply_timeout** - replies to most recent messages in the last 3h * **set base_url** - URL for your Mastodon instance's API * **set commands** - extra commands available in Mastodon channels * **set message_length** - limit messages to 500 characters * **set mode** - create a separate channel for contacts/messages * **set show_ids** - display the "id" in front of every message * **set target_url_length** - an URL counts as 23 characters * **set name** - the name for your account channel * **set hide_sensitive** - hide content marked as sensitive * **set sensitive_flag** - text to flag sensitive content with * **set visibility** - default post privacy * **set hide_boosts** - hide notifications of people boosting * **set hide_favourites** - hide notifications of favourites * **set hide_follows** - hide notifications of follows * **set hide_mentions** - hide notifications of mentions Use **help** to learn more about these options. ## set name > **Type:** string > **Scope:** account > **Default:** empty Without a name set, Mastodon accounts will use host URL and acccount name to create a channel name. This results in a long channel name and if you prefer a shorter channel name, use this setting (when the account is offline) to change it. > **<kensanata>** account mastodon offline > **<kensanata>** account mastodon set name masto > **<kensanata>** account mastodon online > **<kensanata>** save ## set hide_sensitive > **Type:** boolean > **Scope:** account > **Default:** false > **Possible Values:** true, false, rot13, advanced_rot13 By default, sensitive content (content behind a content warning) is simply shown. The content warning is printed, and then the sensitive content is printed. > **<somebody>** [27] [CW: this is the warning] \*NSFW\* this is the text If you set this variable in the control channel (**&bitlbee**), sensitive content is not printed. Instead, you'll see "[hidden: <the URL>]". If you still want to read it, visit the URL. > **<kensanata>** account mastodon set hide_sensitive true > **<kensanata>** save Don't forget to save your settings. The result: > **<somebody>** [27] [CW: this is the warning] \*NSFW\* [hidden: https://social.nasqueron.org/@kensanata/100133795756949791] Additionally, when using **rot13**: > **<somebody>** [27] [CW: this is the warning] \*NSFW\* guvf vf gur grkg And when using **advanced_rot13**: > **<somebody>** [27] [CW: this is the warning] \*NSFW\* > **<somebody>** CW1 guvf vf gur grkg All sensitive content is also marked as Not Safe For Work (NSFW) and flagged as such. You can change the text using an option, see **help set sensitive_flag**). ## set sensitive_flag > **Type:** string > **Scope:** account > **Default:** "\*NSFW\* " This is the text to flag sensitive content with. You can change this setting in the control channel (**&bitlbee**). The default is Not Safe For Work (NSFW). If you wanted to simply use red for the sensitive content, you could use "^C5", for example. Be sure to use an actual Control-C, here. This might be challenging to enter, depending on your IRC client. Sadly, that's how it goes. For more information, see https://www.mirc.com/colors.html. ## set visibility > **Type:** string > **Scope:** account > **Default:** "public" This is the default visibility of your toots. There are three valid options: "public" (everybody can see your toots), "unlisted" (everybody can see your toots but they are not found on the public timelines), and "private" (only followers can see them). For more information see https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md#toot-privacy. This is used whenever you make a new post. You can override this visibility for new posts in the control channel (**&bitlbee**). See *[post](#post)* for more. > **<kensanata>** account mastodon set visibility private > **<kensanata>** save Don't forget to save your settings. ## set hide_boosts > **Type:** boolean > **Scope:** account > **Default:** false > **Possible Values:** true, false By default, you are shown a notification when somebody boosts a status of yours. This setting allows you to turn it off in the control channel (**&bitlbee**). > **<kensanata>** account mastodon set hide_boosts true > **<kensanata>** save Don't forget to save your settings. ## set hide_favourites > **Type:** boolean > **Scope:** account > **Default:** false > **Possible Values:** true, false By default, you are shown a notification when somebody favourites a status of yours. This setting allows you to turn it off in the control channel (**&bitlbee**). > **<kensanata>** account mastodon set hide_favourites true > **<kensanata>** save Don't forget to save your settings. ## set hide_follows > **Type:** boolean > **Scope:** account > **Default:** false > **Possible Values:** true, false By default, you are shown a notification when somebody follows you. This setting allows you to turn it off in the control channel (**&bitlbee**). > **<kensanata>** account mastodon set hide_follows true > **<kensanata>** save Don't forget to save your settings. ## set hide_mentions > **Type:** boolean > **Scope:** account > **Default:** false > **Possible Values:** true, false By default, you are shown a notification when somebody mentions you. This setting allows you to turn it off in the control channel (**&bitlbee**). > **<kensanata>** account mastodon set hide_mentions true > **<kensanata>** save Don't forget to save your settings. ## account add mastodon > **Syntax:** account add mastodon <handle> By default all the Mastodon accounts you are following will appear in a new channel named after your Mastodon instance. You can change this behaviour using the **mode** setting (see **help set mode**). To send toots yourself, just write in the groupchat channel. Since Mastodon requires OAuth authentication, you should not enter your Mastodon password into BitlBee. The first time you log in, BitlBee will start OAuth authentication. (See **help set oauth**.) In order to connect to the correct instances, you must most probably change the **base_url** setting. See *[connect](#connect)* for an example. ## connect In this section, we'll sign in as **@kensanata@mastodon.weaponvsac.space**. This section assumes an existing account on an instance! Replace username and Mastodon server when trying it. In your **&bitlbee** channel, add a new account, change it's **base_url** to point at your instance, and switch it on: > **<kensanata>** account add mastodon @kensanata > **<root>** Account successfully added with tag mastodon > **<kensanata>** account mastodon set base_url https://mastodon.weaponvsac.space/api/v1 > **<root>** base_url = `https://mastodon.weaponvsac.space/api/v1' > **<kensanata>** account mastodon on > **<root>** mastodon - Logging in: Login > **<root>** mastodon - Logging in: Parsing application registration response > **<root>** mastodon - Logging in: Starting OAuth authentication At this point, you'll get contacted by the user **mastodon_oauth** with a big URL that you need to visit using a browser. See *[connect2](#connect2)* for the OAuth authentication. ## connect2 Visit the URL the **mastodon_oauth** user gave you and authenticate the client. You'll get back another very long string. Copy and paste this string: > **<mastodon_oauth>** Open this URL in your browser to authenticate: https://....... > **<mastodon_oauth>** Respond to this message with the returned authorization token. > **<kensanata>** \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* Once you do that, your login should complete in the **&bitlbee** channel: > **<root>** mastodon2 - Logging in: Requesting OAuth access token > **<root>** mastodon2 - Logging in: Connecting > **<root>** mastodon2 - Logging in: Verifying credentials > **<root>** mastodon2 - Logging in: Getting home timeline > **<root>** mastodon2 - Logging in: Logged in You should now have a channel called **#mastodon.weaponsvsac.space@localhost** where all the status updates and notifications get shown. We'll call this your **account channel**. See **help set name** to change it's name. Mastodon gives BitlBee a permanent authentication token, which is saved in your configuration. You should probably save this configuration. > **<kensanata>** save > **<root>** Configuration saved ## read The default **mode** setting is **chat**. This means that each Mastodon account you add results in a new channel in your IRC client. Use **help set mode** in your Bitlbee control channel (**&bitlbee**) to read up on different modes. ## post The default **commands** setting is **true**. This means that anything you type is a toot unless it looks like command, in which case it is handled as such. In addition to that, you can use the **post <message>** command. If you set the **commands** setting to **strict**, using the **post** command is mandatory. Use **help set commands** in your Bitlbee control channel (**&bitlbee**) to read up on the various commands. New posts get the default visibility: "public". See **help set visibility** for more. You can change the visibility of the next toot by using the **visibility** command. > **<kensanata>** visibility unlisted > **<root>** Next post is unlisted > **<kensanata>** Hello! > **<root>** You, unlisted: [06] Hello! Use **cw <content warning>** to set a content warning for your next reply or post. > **<kensanata>** cw capitalism > **<root>** Next post will get content warning 'capitalism' > **<kensanata>** No! Just no! > **<root>** You: [09] [CW: capitalism] \*NSFW\* No! Just no! When mentioning people in your toots, make sure to qualify them appropriately. > **<somebody>** I'm using @kensanata@octodon.social's Mastodon plugin for Bitlbee. By default the Mastodon server limits your toots to 500 characters. Bitlbee tries to compute the message length based on the various Mastodon rules and prevents you from posting longer messages. Use **help set message_length** in your Bitlbee control channel (**&bitlbee**) to read up on the hairy details. Basically, some aspects of of your message will count for less: URLs, domain names for mentioned user accounts and the like. See **help set target_url_length** for more information on how URLs are counted. Note also that Bitlbee itself does word-wrapping to limit messages to 425 characters. That is why longer messages may look like extra newlines have been introduced but if you check the status on the web, you'll see that everything is OK. ## undo Use **undo** and **redo** to undo and redo recent commands. Bitlbee will remember your last 10 Mastodon commands and allows you to undo and redo them. Use **history** to see the list of commands you can undo. There is a pointer (**>**) showing the current position. Use **history undo** if you are interested in seeing the commands that will be used to undo what you just did. ## favourite Use **fav <id|nick>** to favour a status or the last status by a nick. Synonyms: **favourite**, **favorite**, **like**. Use **unfav <id|nick>** to unfavour a status or the last status by a nick. Synonyms: **unfavourite**, **unfavorite**, **unlike**, **dislike**. ## context Use **context <id|nick>** to show some context for a status or the last status by a nick. This will display the ancestors and descendants of a status. Use **timeline <nick>** to show the most recent messages by a nick. Use **more** to show more statuses from the same command. Use **timeline @<account>** to show the most recent messages by an account that isn't a nick in your channel. Use **more** to show more statuses from the same command. ## reply If you use the default IRC conventions of starting a message with a nickname and a colon (**:**) or a comma (**,**), then your message will be treated as a reply to that nick's last message. As is custom, the recipient and all the people they mentioned in their toot get mentioned in your reply. This only works if that nick's last message was sent within the last 3h. For more information about this time window use **help set auto_reply_timeout** in your Bitlbee control channel (**&bitlbee**). You can also reply to an earlier message by referring to its id using the **reply <id> <message>** command. Again, the recipient and all the people they mentioned in their toot get mentioned in your reply. If you set the **commands** setting to **strict**, using the **reply** command is mandatory. When replying to a post, your reply uses the same visibility as the original toot unless your default visibility is more restricted: direct > private > unlisted > public. See **help set visibility** for more. You can set a different visibility using the **visibility** command. See *[post](#post)* for more. When replying to a post, your reply uses the same content warning unless you have set up a different one via **cw**. See *[post](#post)* for more. ## delete Use **del <id>** to delete a status or your last status. Synonym: **delete**. ## favourite Use **fav <id|nick>** to favour a status or the last status by a nick. Synonyms: **favourite**, **favorite**, **like**. Use **unfav <id|nick>** to unfavour a status or the last status by a nick. Synonyms: **unfavourite**, **unfavorite**, **unlike**, **dislike**. ## follow Use **follow <nick|account>** to follow somebody. This determines the nicks in your channel. Verify the list using **/names**. Usually you'll be providing a local or remote account to follow. In the background, Bitlbee will run a search for the account you provided and follow the first match. Sometimes there will be nicks in the channel which you are not following, e.g. a nick is automatically added to the channel when a status of theirs mentioning you is shown. Use **unfollow <nick>** to unfollow a nick. Synonyms: **allow**. ## block Use **block <nick>** to block a nick on the server. This is independent of your IRC client's **/ignore** command, if available. Use **unblock <nick>** to unblock a nick. ## mute Use **mute user <nick>** to mute a nick on the server. Use **unmute user <nick>** to unmute a nick. Use **mute <id|nick>** to mute the conversation based on a status or the last status by a nick. Muting a status will prevent replies to it, favourites and replies of it from appearing. Use **unmute <id|nick>** to unmute the conversation based on a status or the last status by a nick. ## boost Use **boost <id|nick>** to boost a status or the last status by a nick. Use **unboost <id|nick>** to unboost a status or the last status by a nick. ## more Use **url <id|nick>** to get the URL to a status or the last status by a nick. Use **whois <id|nick>** to show handle and full name by a nick, or of all the nicks mentioned in a status. Use **bio <nick>** to show the bio of a nick. Use **pinned <nick>** to show the pinned statuses of a nick. Use **pin <id>** to pin a status to your profile and use **unpin <id>** to unpin a status from your profile. Use **info instance** to get debug information about your instance. Use **info user <nick|account>** to get debug information about an account. Use **info relation <nick|account>** to get debug information about the relation to an account. Use **info <id|nick>** to get debug information about a status or the last status by a nick. Use **api <get|put|post|delete> <endpoint> <optional args>** to call the API directly. > **<kensanata>** api get /lists > **<root>** { > **<root>** id: 5 > **<root>** title: Bots > **<root>** } > **<root>** { > **<root>** id: 9 > **<root>** title: OSR > **<root>** } > **<kensanata>** api delete /lists/5 > **<kensanata>** api get /lists > **<root>** { > **<root>** id: 9 > **<root>** title: OSR > **<root>** } > **<kensanata>** api post /lists/9/accounts account_ids[] 13250 ## search Mastodon allows you to search for accounts, hashtags, and statuses you've written, boosted, favourited or were mentioned in, if your instance has this feature enabled. Use **search <what>** to search for all these things. You can also search for a specific status by searching the URL of said status. This sounds strange but it will allow you to boost it, for example. If you want to show the statuses for a specific account or a hashtag, use **timeline <nick|#hashtag>**. If you want to subscribe to a particular hashtag, see *[hashtag](#hashtag)* for more. ## spam Use **report <id|nick> <comment>** to report a status or the last status by a nick. Synonyms:**spam**. Note that the comment is mandatory. Explain why the status is being reported. The administrator of your instance will see this report and decide what to do about it, if anything. ## control As we said at the beginning, the default **mode** setting is **chat**. This means that each Mastodon account you add will result in a new channel in your IRC client. All the commands mentioned above are what you type in this "instance channel." There are some standard root commands that only work in the control channel, **&bitlbee**. ## hashtag Use **timeline #<hashtag>** to show the most recent messages for a hashtag. Use **more** to show more statuses from the same command. If you want to follow a hashtag, you need to use the control channel, **&bitlbee**. Here's how to subscribe to **#hashtag** for the account **mastodon**: > **<kensanata>** chat add mastodon #hashtag > **<kensanata>** channel #hashtag set auto_join true > **<kensanata>** /join #hashtag Don't forget to **save** your config. Use **channel list** to see all the channels you have available. See **help chat add** for more information. Note that where as you can still issue commands in these hashtag channels, the output is going to appear in the original **account channel**. If you try to subsribe to the same hashtag on a different instance, you'll run into a problem. The solution is to use a different name for the channel and then use **channel <channel> set room #hashtag**. > **<kensanata>** chat add mastodon2 #hashtag > **<root>** A channel named `#hashtag already exists... > **<kensanata>** chat add mastodon2 #hashtag2 > **<kensanata>** channel #hashtag2 set room #hashtag > **<kensanata>** channel #hashtag2 set auto_join true > **<kensanata>** /join #hashtag2 ## public Use **timeline local** to show the most recent messages for the local timeline (these are statuses from accounts on your instance). Use **timeline federated** to show the most recent messages for the federated timeline (these are statuses from the local accounts and anybody they are following). Use **more** to show more statuses from the same command. If you want to follow a hashtag, or the local, or the feredated timeline, you need to use the control channel, **&bitlbee**. The following assumes that your account is called **mastodon**. The **chat add** command takes the parameters **account**, **timeline**, and **channel name**. In the example we're giving the channel a similar name. You can name the channel whatever you want. The important part is that the channel **topic** must be the name of the timeline it is subscribing to. > **<kensanata>** chat add mastodon local #local > **<kensanata>** channel #local set auto_join true > **<kensanata>** /join #local Or: > **<kensanata>** chat add mastodon federated #federated > **<kensanata>** channel #federated set auto_join true > **<kensanata>** /join #federated Don't forget to **save** your config. Note that where as you can still issue commands in these channels, the output is going to appear in the original **account channel**. ## lists You can make lists of the people you follow. To read the latest messages of people in a list, use **timeline <title>**. If you then create a separate channel for a list, you'll see only statuses of people in that list. These are the commands available: > **list** (to see your lists) > **list create <title>** > **list delete <title>** > **list <title>** (to see the accounts in a list) > **list add <nick> to <title>** > **list remove <nick> from <title>** > **list reload** (when you are done changing list memberships) > **timeline <title>** (to read statuses from these accounts) Example: > **<somebody>** follow kensanata@octodon.social \*\*\* kensanata JOIN > **<root>** You are now following kensanata. > **<somebody>** list create Important people > **<root>** Command processed successfully > **<somebody>** list add kensanata to Important people > **<root>** Command processed successfully If you want to follow a list, you need to use the control channel, **&bitlbee**. The following assumes that your account is called **mastodon**. The **chat add** command takes the parameters **account**, **title**, and **channel name**. As list titles may contain spaces, you need to use quotes. You can name the channel whatever you want. The important part is that the channel **topic** must be the title of a list. > **<kensanata>** chat add mastodon "Important people" #important > **<kensanata>** channel #important set auto_join true > **<kensanata>** /join #important Don't forget to **save** your config. ## filters You can create filters which remove toots from the output. These are the commands available: > **filter** (to load and see your filters) > **filter create <phrase>** > **filter delete <n>** Currently filters are created for all contexts, for whole words, and without an expiration date. Fine grained control over the filters created are planned. Furthermore, for whole words, word edges are weird. For a whole word phrase to match, the beginnin and end of the phrase have to be one of a-z, A-Z, or 0-9, and the character before the phrase and after the phrase must not be one of these. Matching does not care about canonical Unicode form, I'm afraid. Example: > **<kensanata>** filter create twitter.com > **<root>** Filter created > **<kensanata>** filter > **<root>** 1. twitter.com (properties: everywhere, server side, whole word) Differences from the Mastodon Web UI: case is significant; toots are filtered even if you look at an account timeline or run a search. ## notifications Use **notifications** to show the most recent notifications again. Use **more** to show more notifications. Note that there are settigns to hide notifications of a particular kind. Once you do that, the **notifications** and **more** commands may show less output, or none at all, as the display of some notifications is suppressed. See **help set hide_boosts**, **help set hide_favourites**, **help set hide_follows**, and **help set hide_mentions**. bitlbee-mastodon-1.4.1/doc/Makefile.am000066400000000000000000000027241343206756200175630ustar00rootroot00000000000000# Copyright 2017 Artem Savkov # Copyright 2017-2018 Alex Schroeder # # 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, see . EXTRA_DIST = mastodon-help.txt all-local: HELP.md install-data-local: if [ -f $(datadir)/help.txt ]; then \ for file in $(EXTRA_DIST); do \ mkdir -p $(DESTDIR)$(datadir); \ $(INSTALL) -m 644 $(srcdir)/$$file $(DESTDIR)$(datadir)/; \ done \ else \ echo "Detected dir $(datadir) is not bitlbee's data dir"; \ echo "Skipping help install"; \ fi HELP.md: mastodon-help.txt sed \ -e '1i# Bitlbee Mastodon\nThis document was generated from the help text for the plugin.\n' \ -e '1d' \ -e 's/^%$$//g' \ -e 's/^\?m/## /g' \ -e 's/^\?/## /g' \ -e 's/\*/\\*/g' \ -e 's//**/g' \ -e 's/^ \*/* */g' \ -e 's/\*help m\([a-z]*2*\)\*/[\1](#\1)/g' \ -e 's//\>/g' \ -e 's/^\(\*[^ ].*\)/> \1 /g' \ < $^ > $@ bitlbee-mastodon-1.4.1/doc/mastodon-help.txt000066400000000000000000000566121343206756200210470ustar00rootroot00000000000000?mastodon Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly. help mnews - Backwards incompatible changes help mregister - Registering an account help mconnect - Connecting to an instance help mread - Reading your timeline help mpost - Posting a new status help mundo - Undo and redo help mcontext - Showing a status in its context help mreply - Replying to a status help mdelete - Deleting a status help mfavourite - Favouring a status help mfollow - Following an account help mblock - Blocking an account help mmute - Muting an account help mboost - Boosting a status help mmore - Getting more information about things help msearch - Searching for accounts and hashtags help mspam - Reporting a status help mcontrol - Commands in the control channel help mhashtag - Showing and following a hashtag help mpublic - Showing and following the local or federated timeline help mlists - Managing lists help mfilters - Managing filters help mnotifications - Showing your notifications help mset - Settings affecting Mastodon accounts % ?mnews Use plugins in the control channel (&bitlbee) to learn which version of the plugin you have. Incompatible change in 1.4.0: If you have subscribed to a hashtag, you need to change your channel settings and prepend the hash. Do this from the control channel (&bitlbee). Let's assume you have a channel called #hashtag. It's room setting should be #hashtag. If it's lacking the initial hash:  channel #hashtag set room  room = `hashtag'  channel #hashtag set room #hashtag  room = `#hashtag' Don't forget to save your settings. % ?mregister You need to register your Mastodon account on an instance. See https://instances.social/ if you need help picking an instance. It's a bit like picking a mail server and signing up. Sadly, there is currently no way to do this from IRC. Your need to use a web browser to do it. Once you have the account, see help account add mastodon for setting up your account. % ?mset These settings will affect Mastodon accounts: set auto_reply_timeout - replies to most recent messages in the last 3h set base_url - URL for your Mastodon instance's API set commands - extra commands available in Mastodon channels set message_length - limit messages to 500 characters set mode - create a separate channel for contacts/messages set show_ids - display the "id" in front of every message set target_url_length - an URL counts as 23 characters set name - the name for your account channel set hide_sensitive - hide content marked as sensitive set sensitive_flag - text to flag sensitive content with set visibility - default post privacy set hide_boosts - hide notifications of people boosting set hide_favourites - hide notifications of favourites set hide_follows - hide notifications of follows set hide_mentions - hide notifications of mentions Use help to learn more about these options. % ?set name Type: string Scope: account Default: empty Without a name set, Mastodon accounts will use host URL and acccount name to create a channel name. This results in a long channel name and if you prefer a shorter channel name, use this setting (when the account is offline) to change it.  account mastodon off  account mastodon set name masto  account mastodon on  save % ?set hide_sensitive Type: boolean Scope: account Default: false Possible Values: true, false, rot13, advanced_rot13 By default, sensitive content (content behind a content warning) is simply shown. The content warning is printed, and then the sensitive content is printed.  [27] [CW: this is the warning] *NSFW* this is the text If you set this variable in the control channel (&bitlbee), sensitive content is not printed. Instead, you'll see "[hidden: ]". If you still want to read it, visit the URL.  account mastodon set hide_sensitive true  save Don't forget to save your settings. The result:  [27] [CW: this is the warning] *NSFW* [hidden: https://social.nasqueron.org/@kensanata/100133795756949791] Additionally, when using rot13:  [27] [CW: this is the warning] *NSFW* guvf vf gur grkg And when using advanced_rot13:  [27] [CW: this is the warning] *NSFW*  CW1 guvf vf gur grkg All sensitive content is also marked as Not Safe For Work (NSFW) and flagged as such. You can change the text using an option, see help set sensitive_flag). % ?set sensitive_flag Type: string Scope: account Default: "*NSFW* " This is the text to flag sensitive content with. You can change this setting in the control channel (&bitlbee). The default is Not Safe For Work (NSFW). If you wanted to simply use red for the sensitive content, you could use "^C5", for example. Be sure to use an actual Control-C, here. This might be challenging to enter, depending on your IRC client. Sadly, that's how it goes. For more information, see https://www.mirc.com/colors.html. % ?set visibility Type: string Scope: account Default: "public" This is the default visibility of your toots. There are three valid options: "public" (everybody can see your toots), "unlisted" (everybody can see your toots but they are not found on the public timelines), and "private" (only followers can see them). For more information see https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md#toot-privacy. This is used whenever you make a new post. You can override this visibility for new posts in the control channel (&bitlbee). See help mpost for more.  account mastodon set visibility private  save Don't forget to save your settings. % ?set hide_boosts Type: boolean Scope: account Default: false Possible Values: true, false By default, you are shown a notification when somebody boosts a status of yours. This setting allows you to turn it off in the control channel (&bitlbee).  account mastodon set hide_boosts true  save Don't forget to save your settings. % ?set hide_favourites Type: boolean Scope: account Default: false Possible Values: true, false By default, you are shown a notification when somebody favourites a status of yours. This setting allows you to turn it off in the control channel (&bitlbee).  account mastodon set hide_favourites true  save Don't forget to save your settings. % ?set hide_follows Type: boolean Scope: account Default: false Possible Values: true, false By default, you are shown a notification when somebody follows you. This setting allows you to turn it off in the control channel (&bitlbee).  account mastodon set hide_follows true  save Don't forget to save your settings. % ?set hide_mentions Type: boolean Scope: account Default: false Possible Values: true, false By default, you are shown a notification when somebody mentions you. This setting allows you to turn it off in the control channel (&bitlbee).  account mastodon set hide_mentions true  save Don't forget to save your settings. % ?account add mastodon Syntax: account add mastodon By default all the Mastodon accounts you are following will appear in a new channel named after your Mastodon instance. You can change this behaviour using the mode setting (see help set mode). To send toots yourself, just write in the groupchat channel. Since Mastodon requires OAuth authentication, you should not enter your Mastodon password into BitlBee. The first time you log in, BitlBee will start OAuth authentication. (See help set oauth.) In order to connect to the correct instances, you must most probably change the base_url setting. See help mconnect for an example. % ?mconnect In this section, we'll sign in as @kensanata@mastodon.weaponvsac.space. This section assumes an existing account on an instance! Replace username and Mastodon server when trying it. In your &bitlbee channel, add a new account, change it's base_url to point at your instance, and switch it on:  account add mastodon @kensanata  Account successfully added with tag mastodon  account mastodon set base_url https://mastodon.weaponvsac.space/api/v1  base_url = `https://mastodon.weaponvsac.space/api/v1'  account mastodon on  mastodon - Logging in: Login  mastodon - Logging in: Parsing application registration response  mastodon - Logging in: Starting OAuth authentication At this point, you'll get contacted by the user mastodon_oauth with a big URL that you need to visit using a browser. See help mconnect2 for the OAuth authentication. % ?mconnect2 Visit the URL the mastodon_oauth user gave you and authenticate the client. You'll get back another very long string. Copy and paste this string:  Open this URL in your browser to authenticate: https://.......  Respond to this message with the returned authorization token.  **************************************************************** Once you do that, your login should complete in the &bitlbee channel:  mastodon2 - Logging in: Requesting OAuth access token  mastodon2 - Logging in: Connecting  mastodon2 - Logging in: Verifying credentials  mastodon2 - Logging in: Getting home timeline  mastodon2 - Logging in: Logged in You should now have a channel called #mastodon.weaponsvsac.space@localhost where all the status updates and notifications get shown. We'll call this your account channel. See help set name to change it's name. Mastodon gives BitlBee a permanent authentication token, which is saved in your configuration. You should probably save this configuration.  save  Configuration saved % ?mread The default mode setting is chat. This means that each Mastodon account you add results in a new channel in your IRC client. Use help set mode in your Bitlbee control channel (&bitlbee) to read up on different modes. % ?mpost The default commands setting is true. This means that anything you type is a toot unless it looks like command, in which case it is handled as such. In addition to that, you can use the post  command. If you set the commands setting to strict, using the post command is mandatory. Use help set commands in your Bitlbee control channel (&bitlbee) to read up on the various commands. New posts get the default visibility: "public". See help set visibility for more. You can change the visibility of the next toot by using the visibility command.  visibility unlisted  Next post is unlisted  Hello!  You, unlisted: [06] Hello! Use cw  to set a content warning for your next reply or post.  cw capitalism  Next post will get content warning 'capitalism'  No! Just no!  You: [09] [CW: capitalism] *NSFW* No! Just no! When mentioning people in your toots, make sure to qualify them appropriately.  I'm using @kensanata@octodon.social's Mastodon plugin for Bitlbee. By default the Mastodon server limits your toots to 500 characters. Bitlbee tries to compute the message length based on the various Mastodon rules and prevents you from posting longer messages. Use help set message_length in your Bitlbee control channel (&bitlbee) to read up on the hairy details. Basically, some aspects of of your message will count for less: URLs, domain names for mentioned user accounts and the like. See help set target_url_length for more information on how URLs are counted. Note also that Bitlbee itself does word-wrapping to limit messages to 425 characters. That is why longer messages may look like extra newlines have been introduced but if you check the status on the web, you'll see that everything is OK. % ?mundo Use undo and redo to undo and redo recent commands. Bitlbee will remember your last 10 Mastodon commands and allows you to undo and redo them. Use history to see the list of commands you can undo. There is a pointer (>) showing the current position. Use history undo if you are interested in seeing the commands that will be used to undo what you just did. % ?mfavourite Use fav  to favour a status or the last status by a nick. Synonyms: favourite, favorite, like. Use unfav  to unfavour a status or the last status by a nick. Synonyms: unfavourite, unfavorite, unlike, dislike. % ?mcontext Use context  to show some context for a status or the last status by a nick. This will display the ancestors and descendants of a status. Use timeline  to show the most recent messages by a nick. Use more to show more statuses from the same command. Use timeline @ to show the most recent messages by an account that isn't a nick in your channel. Use more to show more statuses from the same command. % ?mreply If you use the default IRC conventions of starting a message with a nickname and a colon (:) or a comma (,), then your message will be treated as a reply to that nick's last message. As is custom, the recipient and all the people they mentioned in their toot get mentioned in your reply. This only works if that nick's last message was sent within the last 3h. For more information about this time window use help set auto_reply_timeout in your Bitlbee control channel (&bitlbee). You can also reply to an earlier message by referring to its id using the reply  command. Again, the recipient and all the people they mentioned in their toot get mentioned in your reply. If you set the commands setting to strict, using the reply command is mandatory. When replying to a post, your reply uses the same visibility as the original toot unless your default visibility is more restricted: direct > private > unlisted > public. See help set visibility for more. You can set a different visibility using the visibility command. See help mpost for more. When replying to a post, your reply uses the same content warning unless you have set up a different one via cw. See help mpost for more. % ?mdelete Use del  to delete a status or your last status. Synonym: delete. % ?mfavourite Use fav  to favour a status or the last status by a nick. Synonyms: favourite, favorite, like. Use unfav  to unfavour a status or the last status by a nick. Synonyms: unfavourite, unfavorite, unlike, dislike. % ?mfollow Use follow  to follow somebody. This determines the nicks in your channel. Verify the list using /names. Usually you'll be providing a local or remote account to follow. In the background, Bitlbee will run a search for the account you provided and follow the first match. Sometimes there will be nicks in the channel which you are not following, e.g. a nick is automatically added to the channel when a status of theirs mentioning you is shown. Use unfollow  to unfollow a nick. Synonyms: allow. % ?mblock Use block  to block a nick on the server. This is independent of your IRC client's /ignore command, if available. Use unblock  to unblock a nick. % ?mmute Use mute user  to mute a nick on the server. Use unmute user  to unmute a nick. Use mute  to mute the conversation based on a status or the last status by a nick. Muting a status will prevent replies to it, favourites and replies of it from appearing. Use unmute  to unmute the conversation based on a status or the last status by a nick. % ?mboost Use boost  to boost a status or the last status by a nick. Use unboost  to unboost a status or the last status by a nick. % ?mmore Use url  to get the URL to a status or the last status by a nick. Use whois  to show handle and full name by a nick, or of all the nicks mentioned in a status. Use bio  to show the bio of a nick. Use pinned  to show the pinned statuses of a nick. Use pin  to pin a status to your profile and use unpin  to unpin a status from your profile. Use info instance to get debug information about your instance. Use info user  to get debug information about an account. Use info relation  to get debug information about the relation to an account. Use info  to get debug information about a status or the last status by a nick. Use api  to call the API directly.  api get /lists  {  id: 5  title: Bots  }  {  id: 9  title: OSR  }  api delete /lists/5  api get /lists  {  id: 9  title: OSR  }  api post /lists/9/accounts account_ids[] 13250 % ?msearch Mastodon allows you to search for accounts, hashtags, and statuses you've written, boosted, favourited or were mentioned in, if your instance has this feature enabled. Use search  to search for all these things. You can also search for a specific status by searching the URL of said status. This sounds strange but it will allow you to boost it, for example. If you want to show the statuses for a specific account or a hashtag, use timeline . If you want to subscribe to a particular hashtag, see help mhashtag for more. % ?mspam Use report  to report a status or the last status by a nick. Synonyms:spam. Note that the comment is mandatory. Explain why the status is being reported. The administrator of your instance will see this report and decide what to do about it, if anything. % ?mcontrol As we said at the beginning, the default mode setting is chat. This means that each Mastodon account you add will result in a new channel in your IRC client. All the commands mentioned above are what you type in this "instance channel." There are some standard root commands that only work in the control channel, &bitlbee. % ?mhashtag Use timeline # to show the most recent messages for a hashtag. Use more to show more statuses from the same command. If you want to follow a hashtag, you need to use the control channel, &bitlbee. Here's how to subscribe to #hashtag for the account mastodon:  chat add mastodon #hashtag  channel #hashtag set auto_join true  /join #hashtag Don't forget to save your config. Use channel list to see all the channels you have available. See help chat add for more information. Note that where as you can still issue commands in these hashtag channels, the output is going to appear in the original account channel. If you try to subsribe to the same hashtag on a different instance, you'll run into a problem. The solution is to use a different name for the channel and then use channel set room #hashtag.  chat add mastodon2 #hashtag  A channel named `#hashtag already exists...  chat add mastodon2 #hashtag2  channel #hashtag2 set room #hashtag  channel #hashtag2 set auto_join true  /join #hashtag2 % ?mpublic Use timeline local to show the most recent messages for the local timeline (these are statuses from accounts on your instance). Use timeline federated to show the most recent messages for the federated timeline (these are statuses from the local accounts and anybody they are following). Use more to show more statuses from the same command. If you want to follow a hashtag, or the local, or the feredated timeline, you need to use the control channel, &bitlbee. The following assumes that your account is called mastodon. The chat add command takes the parameters account, timeline, and channel name. In the example we're giving the channel a similar name. You can name the channel whatever you want. The important part is that the channel topic must be the name of the timeline it is subscribing to.  chat add mastodon local #local  channel #local set auto_join true  /join #local Or:  chat add mastodon federated #federated  channel #federated set auto_join true  /join #federated Don't forget to save your config. Note that where as you can still issue commands in these channels, the output is going to appear in the original account channel. % ?mlists You can make lists of the people you follow. To read the latest messages of people in a list, use timeline . If you then create a separate channel for a list, you'll see only statuses of people in that list. These are the commands available: list (to see your lists) list create <title> list delete <title> list <title> (to see the accounts in a list) list add <nick> to <title> list remove <nick> from <title> list reload (when you are done changing list memberships) timeline <title> (to read statuses from these accounts) Example: <somebody> follow kensanata@octodon.social *** kensanata JOIN <root> You are now following kensanata. <somebody> list create Important people <root> Command processed successfully <somebody> list add kensanata to Important people <root> Command processed successfully If you want to follow a list, you need to use the control channel, &bitlbee. The following assumes that your account is called mastodon. The chat add command takes the parameters account, title, and channel name. As list titles may contain spaces, you need to use quotes. You can name the channel whatever you want. The important part is that the channel topic must be the title of a list. <kensanata> chat add mastodon "Important people" #important <kensanata> channel #important set auto_join true <kensanata> /join #important Don't forget to save your config. % ?mfilters You can create filters which remove toots from the output. These are the commands available: filter (to load and see your filters) filter create <phrase> filter delete <n> Currently filters are created for all contexts, for whole words, and without an expiration date. Fine grained control over the filters created are planned. Furthermore, for whole words, word edges are weird. For a whole word phrase to match, the beginnin and end of the phrase have to be one of a-z, A-Z, or 0-9, and the character before the phrase and after the phrase must not be one of these. Matching does not care about canonical Unicode form, I'm afraid. Example: <kensanata> filter create twitter.com <root> Filter created <kensanata> filter <root> 1. twitter.com (properties: everywhere, server side, whole word) Differences from the Mastodon Web UI: case is significant; toots are filtered even if you look at an account timeline or run a search. % ?mnotifications Use notifications to show the most recent notifications again. Use more to show more notifications. Note that there are settigns to hide notifications of a particular kind. Once you do that, the notifications and more commands may show less output, or none at all, as the display of some notifications is suppressed. See help set hide_boosts, help set hide_favourites, help set hide_follows, and help set hide_mentions. % ����������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/�������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13432067562�0015544�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/Makefile.am��������������������������������������������������������������0000664�0000000�0000000�00000002110�13432067562�0017572�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2016 Artem Savkov <artem.savkov@gmail.com> # Copyright 2017 Alex Schroeder <alex@gnu.org> # # 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, see <http://www.gnu.org/licenses/>. libdir = $(plugindir) lib_LTLIBRARIES = mastodon.la mastodon_la_CFLAGS = \ $(BITLBEE_CFLAGS) \ $(GLIB_CFLAGS) \ -Wall mastodon_la_LDFLAGS = \ -module \ -avoid-version \ $(BITLBEE_LIBS) \ $(GLIB_LIBS) mastodon_la_SOURCES = \ mastodon.c \ mastodon.h \ mastodon-http.c \ mastodon-http.h \ mastodon-lib.c \ mastodon-lib.h \ rot13.c \ rot13.h ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon-http.c����������������������������������������������������������0000664�0000000�0000000�00000012513�13432067562�0020513�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2017 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ /***************************************************************************\ * * * Some functions within this file have been copied from other files within * * BitlBee. * * * ****************************************************************************/ #include "mastodon.h" #include "bitlbee.h" #include "url.h" #include "misc.h" #include "base64.h" #include "oauth.h" #include <ctype.h> #include <errno.h> #include "mastodon-http.h" static char *mastodon_url_append(char *url, char *key, char *value) { char *key_encoded = g_strndup(key, 3 * strlen(key)); http_encode(key_encoded); char *value_encoded = g_strndup(value, 3 * strlen(value)); http_encode(value_encoded); char *retval; if (strlen(url) != 0) { retval = g_strdup_printf("%s&%s=%s", url, key_encoded, value_encoded); } else { retval = g_strdup_printf("%s=%s", key_encoded, value_encoded); } g_free(key_encoded); g_free(value_encoded); return retval; } /** * Do a request. * This is actually pretty generic function... Perhaps it should move to the lib/http_client.c */ struct http_request *mastodon_http(struct im_connection *ic, char *url_string, http_input_function func, gpointer data, http_method_t method, char **arguments, int arguments_len) { struct mastodon_data *md = ic->proto_data; void *ret = NULL; char *url_arguments = g_strdup(""); char *request_method = "GET"; switch (method) { case HTTP_GET: request_method = "GET"; break; case HTTP_PUT: request_method = "PUT"; break; case HTTP_POST: request_method = "POST"; break; case HTTP_DELETE: request_method = "DELETE"; break; } // Construct the url arguments. if (arguments_len != 0) { int i; for (i = 0; i < arguments_len; i += 2) { char *tmp = mastodon_url_append(url_arguments, arguments[i], arguments[i + 1]); g_free(url_arguments); url_arguments = tmp; } } url_t *base_url = NULL; if (strstr(url_string, "://")) { base_url = g_new0(url_t, 1); if (!url_set(base_url, url_string)) { goto error; } } // Make the request. GString *request = g_string_new(""); g_string_printf(request, "%s %s%s%s%s HTTP/1.1\r\n" "Host: %s\r\n" "User-Agent: BitlBee " BITLBEE_VERSION "\r\n" "Authorization: Bearer %s\r\n", request_method, base_url ? base_url->file : md->url_path, base_url ? "" : url_string, method == HTTP_GET && url_arguments[0] ? "?" : "", method == HTTP_GET && url_arguments[0] ? url_arguments : "", base_url ? base_url->host : md->url_host, md->oauth2_access_token); // Do POST stuff.. if (method != HTTP_GET) { // Append the Content-Type and url-encoded arguments. g_string_append_printf(request, "Content-Type: application/x-www-form-urlencoded\r\n" "Content-Length: %zd\r\n\r\n%s", strlen(url_arguments), url_arguments); } else { // Append an extra \r\n to end the request... g_string_append(request, "\r\n"); } if (base_url) { ret = http_dorequest(base_url->host, base_url->port, base_url->proto == PROTO_HTTPS, request->str, func, data); } else { ret = http_dorequest(md->url_host, md->url_port, md->url_ssl, request->str, func, data); } g_string_free(request, TRUE); error: g_free(url_arguments); g_free(base_url); return ret; } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon-http.h����������������������������������������������������������0000664�0000000�0000000�00000004146�13432067562�0020523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2017 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ #pragma once #include "nogaim.h" #include "http_client.h" typedef enum { HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_DELETE, } http_method_t; struct http_request *mastodon_http(struct im_connection *ic, char *url_string, http_input_function func, gpointer data, http_method_t method, char** arguments, int arguments_len); ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon-lib.c�����������������������������������������������������������0000664�0000000�0000000�00000362126�13432067562�0020312�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net> * * Copyright 2017-2018 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ /* For strptime(): */ #if (__sun) #else #define _XOPEN_SOURCE #endif #include "mastodon-http.h" #include "mastodon.h" #include "rot13.h" #include "bitlbee.h" #include "url.h" #include "misc.h" #include "base64.h" #include "mastodon-lib.h" #include "oauth2.h" #include "json_util.h" #include <assert.h> #include <ctype.h> #include <errno.h> typedef enum { MT_HOME, MT_LOCAL, MT_FEDERATED, MT_HASHTAG, MT_LIST, } mastodon_timeline_type_t; typedef enum { ML_STATUS, ML_NOTIFICATION, } mastodon_list_type_t; struct mastodon_list { mastodon_list_type_t type; GSList *list; }; struct mastodon_account { guint64 id; char *display_name; char *acct; }; struct mastodon_status { time_t created_at; char *spoiler_text; char *spoiler_text_case_folded; /* for filtering */ char *text; char *content; /* same as text without CW and NSFW prefixes */ char *content_case_folded; /* for filtering */ char *url; struct mastodon_account *account; guint64 id; mastodon_visibility_t visibility; guint64 reply_to; GSList *tags; GSList *mentions; mastodon_timeline_type_t subscription; /* This status was created by a timeline subscription */ gboolean is_notification; /* This status was created from a notification */ }; typedef enum { MN_MENTION = 1, MN_REBLOG, MN_FAVOURITE, MN_FOLLOW, } mastodon_notification_type_t; struct mastodon_notification { guint64 id; mastodon_notification_type_t type; time_t created_at; struct mastodon_account *account; struct mastodon_status *status; }; struct mastodon_report { struct im_connection *ic; guint64 account_id; guint64 status_id; char *comment; }; typedef enum { MF_HOME = 0x00001, MF_NOTIFICATIONS = 0x00002, MF_PUBLIC = 0x00004, MF_THREAD = 0x00008, } mastodon_filter_type_t; struct mastodon_filter { guint64 id; char* phrase; char* phrase_case_folded; mastodon_filter_type_t context; gboolean irreversible; gboolean whole_word; time_t expires_in; }; struct mastodon_command { struct im_connection *ic; guint64 id; guint64 id2; gboolean extra; char *str; char *undo; char *redo; gpointer *data; mastodon_command_type_t command; }; /** * Frees a mastodon_account struct. */ static void ma_free(struct mastodon_account *ma) { if (ma == NULL) { return; } g_free(ma->display_name); g_free(ma->acct); g_free(ma); } /** * Creates a duplicate of an account. */ static struct mastodon_account *ma_copy(struct mastodon_account *ma0) { if (ma0 == NULL) { return NULL; } struct mastodon_account *ma = g_new0(struct mastodon_account, 1); ma->id = ma0->id; ma->display_name = g_strdup(ma0->display_name); ma->acct = g_strdup(ma0->acct); return ma; } /** * Frees a mastodon_status struct. */ static void ms_free(struct mastodon_status *ms) { if (ms == NULL) { return; } g_free(ms->spoiler_text); g_free(ms->text); g_free(ms->content); g_free(ms->url); ma_free(ms->account); g_slist_free_full(ms->tags, g_free); g_slist_free_full(ms->mentions, (GDestroyNotify) ma_free); g_free(ms); } /** * Frees a mastodon_notification struct. */ static void mn_free(struct mastodon_notification *mn) { if (mn == NULL) { return; } ma_free(mn->account); ms_free(mn->status); g_free(mn); } /** * Free a mastodon_list struct. * type is the type of list the struct holds. */ static void ml_free(struct mastodon_list *ml) { GSList *l; if (ml == NULL) { return; } for (l = ml->list; l; l = g_slist_next(l)) { if (ml->type == ML_STATUS) { ms_free((struct mastodon_status *) l->data); } else if (ml->type == ML_NOTIFICATION) { mn_free((struct mastodon_notification *) l->data); } } g_slist_free(ml->list); g_free(ml); } /** * Frees a mastodon_report struct. */ static void mr_free(struct mastodon_report *mr) { if (mr == NULL) { return; } g_free(mr->comment); g_free(mr); } /** * Frees a mastodon_filter struct. */ static void mf_free(struct mastodon_filter *mf) { if (mf == NULL) { return; } g_free(mf->phrase); g_free(mf); } /** * Frees a mastodon_command struct. Note that the groupchat c doesn't need to be freed. It is maintained elsewhere. */ static void mc_free(struct mastodon_command *mc) { if (mc == NULL) { return; } g_free(mc->str); g_free(mc->undo); g_free(mc->redo); g_free(mc); } /** * Compare status elements */ static gint mastodon_compare_elements(gconstpointer a, gconstpointer b) { struct mastodon_status *a_status = (struct mastodon_status *) a; struct mastodon_status *b_status = (struct mastodon_status *) b; if (a_status->created_at < b_status->created_at) { return -1; } else if (a_status->created_at > b_status->created_at) { return 1; } else { return 0; } } /** * Add a buddy if it is not already added, set the status to logged in. */ static void mastodon_add_buddy(struct im_connection *ic, gint64 id, char *name, const char *fullname) { struct mastodon_data *md = ic->proto_data; // Check if the buddy is already in the buddy list. if (!bee_user_by_handle(ic->bee, ic, name)) { // The buddy is not in the list, add the buddy and set the status to logged in. imcb_add_buddy(ic, name, NULL); imcb_rename_buddy(ic, name, fullname); bee_user_t *bu = bee_user_by_handle(ic->bee, ic, name); struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data; mud->account_id = id; if (md->flags & MASTODON_MODE_CHAT) { /* Necessary so that nicks always get translated to the exact Mastodon username. */ imcb_buddy_nick_hint(ic, name, name); if (md->timeline_gc) { imcb_chat_add_buddy(md->timeline_gc, name); } } else if (md->flags & MASTODON_MODE_MANY) { imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL); } } } /* Warning: May return a malloc()ed value, which will be free()d on the next call. Only for short-term use. NOT THREADSAFE! */ char *mastodon_parse_error(struct http_request *req) { static char *ret = NULL; json_value *root, *err; g_free(ret); ret = NULL; if (req->body_size > 0) { root = json_parse(req->reply_body, req->body_size); err = json_o_get(root, "error"); if (err && err->type == json_string && err->u.string.length) { ret = g_strdup_printf("%s (%s)", req->status_string, err->u.string.ptr); } json_value_free(root); } return ret ? ret : req->status_string; } /* WATCH OUT: This function might or might not destroy your connection. Sub-optimal indeed, but just be careful when this returns NULL! */ static json_value *mastodon_parse_response(struct im_connection *ic, struct http_request *req) { char path[64] = "", *s; if ((s = strchr(req->request, ' '))) { path[sizeof(path) - 1] = '\0'; strncpy(path, s + 1, sizeof(path) - 1); if ((s = strchr(path, '?')) || (s = strchr(path, ' '))) { *s = '\0'; } } if (req->status_code != 200) { mastodon_log(ic, "Error: %s returned status code %s", path, mastodon_parse_error(req)); if (!(ic->flags & OPT_LOGGED_IN)) { imc_logout(ic, TRUE); } return NULL; } json_value *ret; if ((ret = json_parse(req->reply_body, req->body_size)) == NULL) { imcb_error(ic, "Error: %s return data that could not be parsed as JSON", path); } return ret; } /** * For Mastodon 2, all id attributes in the REST API responses, including attributes that end in _id, are now returned * as strings instead of integers. This is because large integers cannot be encoded in JSON losslessly, and all IDs in * Mastodon are now bigint (Ruby on Rails: bigint uses 64 bits, signed, guint64 is 64 bits, unsigned). We are assuming * no negative ids. */ static guint64 mastodon_json_int64(const json_value *v) { guint64 id; if (v->type == json_integer) { return v->u.integer; // Mastodon 1 } else if (v->type == json_string && *v->u.string.ptr && parse_int64(v->u.string.ptr, 10, &id)) { return id; // Mastodon 2 } return 0; } /* These two functions are useful to debug all sorts of callbacks. */ static void mastodon_log_object(struct im_connection *ic, json_value *node, int prefix); static void mastodon_log_array(struct im_connection *ic, json_value *node, int prefix); struct mastodon_account *mastodon_xt_get_user(const json_value *node) { struct mastodon_account *ma; json_value *jv; ma = g_new0(struct mastodon_account, 1); ma->display_name = g_strdup(json_o_str(node, "display_name")); ma->acct = g_strdup(json_o_str(node, "acct")); if ((jv = json_o_get(node, "id")) && (ma->id = mastodon_json_int64(jv))) { return ma; } ma_free(ma); return NULL; } /* This is based on strip_html but in addition to what Bitlbee does, we treat p like br. */ void mastodon_strip_html(char *in) { char *start = in; char out[strlen(in) + 1]; char *s = out; memset(out, 0, sizeof(out)); while (*in) { if (*in == '<') { if (g_strncasecmp(in + 1, "/p>", 3) == 0) { *(s++) = '\n'; in += 4; } else { *(s++) = *(in++); } } else { *(s++) = *(in++); } } strcpy(start, out); strip_html(start); } mastodon_visibility_t mastodon_parse_visibility(char *value) { if (g_strcasecmp(value, "public") == 0) { return MV_PUBLIC; } else if (g_strcasecmp(value, "unlisted") == 0) { return MV_UNLISTED; } else if (g_strcasecmp(value, "private") == 0) { return MV_PRIVATE; } else if (g_strcasecmp(value, "direct") == 0) { return MV_DIRECT; } else { return MV_UNKNOWN; } } /** * Here, we have a similar setup as for mastodon_chained_account_function. The flow is as follows: the * mastodon_command_handler() calls mastodon_unknown_list_delete(). This sets up the mastodon_command (mc). It then * calls mastodon_with_named_list() and passes along a callback, mastodon_http_list_delete(). It uses * mastodon_chained_list() to extract the list id and store it in mc, and calls the next handler, * mastodon_list_delete(). This is a mastodon_chained_command_function! It doesn't have to check whether ic is live. */ typedef void (*mastodon_chained_command_function)(struct im_connection *ic, struct mastodon_command *mc); /** * This is the wrapper around callbacks that need to search for the list id in a list result. Note that list titles are * case-sensitive. */ static void mastodon_chained_list(struct http_request *req, mastodon_chained_command_function func) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { goto finally; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finally; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "You seem to have no lists defined. " "Create one using 'list create <title>'."); goto finish; } int i; guint64 id = 0; char *title = mc->str; for (i = 0; i < parsed->u.array.length; i++) { json_value *a = parsed->u.array.values[i]; json_value *it; if (a->type == json_object && (it = json_o_get(a, "id")) && g_strcmp0(title, json_o_str(a, "title")) == 0) { id = mastodon_json_int64(it); break; } } if (!id) { mastodon_log(ic, "There is no list called '%s'. " "Use 'list' to show existing lists.", title); goto finish; } else { mc->id = id; func(ic, mc); /* If successful, we need to keep mc for one more request. */ json_value_free(parsed); return; } finish: /* We didn't find what we were looking for and need to free the parsed data. */ json_value_free(parsed); finally: /* We've encountered a problem and we need to free the mastodon_command. */ mc_free(mc); } /** * Wrapper which sets up the first callback for functions acting on a list. For every command, the list has to be * searched, first. The callback you provide must used mastodon_chained_list() to extract the list id and then call the * reall callback. */ void mastodon_with_named_list(struct im_connection *ic, struct mastodon_command *mc, http_input_function func) { mastodon_http(ic, MASTODON_LIST_URL, func, mc, HTTP_GET, NULL, 0); } /** * Function to fill a mastodon_status struct. */ static struct mastodon_status *mastodon_xt_get_status(const json_value *node, struct im_connection *ic) { struct mastodon_status *ms = {0}; const json_value *rt = NULL; const json_value *text_value = NULL; const json_value *spoiler_value = NULL; const json_value *url_value = NULL; GSList *media = NULL; gboolean nsfw = FALSE; gboolean use_cw1 = g_strcasecmp(set_getstr(&ic->acc->set, "hide_sensitive"), "advanced_rot13") == 0; if (node->type != json_object) { return FALSE; } ms = g_new0(struct mastodon_status, 1); JSON_O_FOREACH(node, k, v) { if (strcmp("content", k) == 0 && v->type == json_string && *v->u.string.ptr) { text_value = v; } if (strcmp("spoiler_text", k) == 0 && v->type == json_string && *v->u.string.ptr) { spoiler_value = v; } else if (strcmp("url", k) == 0 && v->type == json_string) { url_value = v; } else if (strcmp("reblog", k) == 0 && v->type == json_object) { rt = v; } else if (strcmp("created_at", k) == 0 && v->type == json_string) { struct tm parsed; /* Very sensitive to changes to the formatting of this field. :-( Also assumes the timezone used is UTC since C time handling functions suck. */ if (strptime(v->u.string.ptr, MASTODON_TIME_FORMAT, &parsed) != NULL) { ms->created_at = mktime_utc(&parsed); } } else if (strcmp("visibility", k) == 0 && v->type == json_string && *v->u.string.ptr) { ms->visibility = mastodon_parse_visibility(v->u.string.ptr); } else if (strcmp("account", k) == 0 && v->type == json_object) { ms->account = mastodon_xt_get_user(v); } else if (strcmp("id", k) == 0) { ms->id = mastodon_json_int64(v); } else if (strcmp("in_reply_to_id", k) == 0) { ms->reply_to = mastodon_json_int64(v); } else if (strcmp("tags", k) == 0 && v->type == json_array) { GSList *l = NULL; int i; for (i = 0; i < v->u.array.length; i++) { json_value *tag = v->u.array.values[i]; if (tag->type == json_object) { const char *name = json_o_str(tag, "name"); if (name) { l = g_slist_prepend(l, g_strdup(name)); } } } ms->tags = l; } else if (strcmp("mentions", k) == 0 && v->type == json_array) { GSList *l = NULL; int i; gint64 id = set_getint(&ic->acc->set, "account_id"); for (i = 0; i < v->u.array.length; i++) { struct mastodon_account *ma = mastodon_xt_get_user(v->u.array.values[i]); /* Skip the current user in mentions since we're only interested in this information for replies where * we'll never want to mention ourselves. */ if (ma && ma->id != id) l = g_slist_prepend(l, ma); } ms->mentions = l; } else if (strcmp("sensitive", k) == 0 && v->type == json_boolean) { nsfw = v->u.boolean; } else if (strcmp("media_attachments", k) == 0 && v->type == json_array) { int i; for (i = 0; i < v->u.array.length; i++) { json_value *attachment = v->u.array.values[i]; if (attachment->type == json_object) { // text_url is preferred because that's what the UI also copies // into the message; also ignore values such as /files/original/missing.png const char *url = json_o_str(attachment, "text_url"); if (!url || !*url || strncmp(url, "http", 4)) { url = json_o_str(attachment, "url"); if (!url || !*url || strncmp(url, "http", 4)) { url = json_o_str(attachment, "remote_url"); } } if (url && *url && strncmp(url, "http", 4) == 0) { media = g_slist_prepend(media, (char *) url); // discarding const qualifier } } } } } if (rt) { struct mastodon_status *rms = mastodon_xt_get_status(rt, ic); if (rms) { /* Alternatively, we could free ms and just use rms, but we'd have to overwrite rms->account * with ms->account, change rms->text, and maybe more. */ ms->text = g_strdup_printf("boosted @%s: %s", rms->account->acct, rms->text); ms->id = rms->id; ms->url = rms->url; // adopt rms->url = NULL; g_slist_free_full(ms->tags, g_free); ms->tags = rms->tags; // adopt rms->tags = NULL; g_slist_free_full(ms->mentions, (GDestroyNotify) ma_free); ms->mentions = rms->mentions; // adopt rms->mentions = NULL; /* add original author to mentions of boost if not ourselves */ gint64 id = set_getint(&ic->acc->set, "account_id"); if (rms->account->id != id) { ms->mentions = g_slist_prepend(ms->mentions, rms->account); // adopt rms->account = NULL; } ms_free(rms); } } else if (ms->id) { if (url_value) { ms->url = g_strdup(url_value->u.string.ptr); } // build status text GString *s = g_string_new(NULL); if (spoiler_value) { char *spoiler_text = g_strdup(spoiler_value->u.string.ptr); mastodon_strip_html(spoiler_text); g_string_append_printf(s, "[CW: %s]", spoiler_text); ms->spoiler_text = spoiler_text; ms->spoiler_text_case_folded = g_utf8_casefold(spoiler_text, -1); if (nsfw || !use_cw1) { g_string_append(s, " "); } } if (nsfw) { char *sensitive_flag = set_getstr(&ic->acc->set, "sensitive_flag"); g_string_append(s, sensitive_flag); } if (text_value) { char *text = g_strdup(text_value->u.string.ptr); mastodon_strip_html(text); ms->content = g_strdup(text); ms->content_case_folded = g_utf8_casefold(text, -1); char *fmt = "%s"; if (spoiler_value && use_cw1) { char *wrapped = NULL; char **cwed = NULL; rot13(text); // "\001CW1 \001" = 6 bytes, there's also a nick length issue we take into account. // there's also irc_format_timestamp which can add like 28 bytes or something. wrapped = word_wrap(text, IRC_WORD_WRAP - 6 - MAX_NICK_LENGTH - 28); g_free(text); text = wrapped; cwed = g_strsplit(text, "\n", -1); // easier than a regex g_free(text); text = g_strjoinv("\001\n\001CW1 ", cwed); // easier than a replace g_strfreev(cwed); fmt = "\n\001CW1 %s\001"; // add a newline at the start because that makes word wrap a lot easier (and because it matches the web UI better) } else if (spoiler_value && g_strcasecmp(set_getstr(&ic->acc->set, "hide_sensitive"), "rot13") == 0) { rot13(text); } else if (spoiler_value && set_getbool(&ic->acc->set, "hide_sensitive")) { g_free(text); text = g_strdup(ms->url); if (text) { fmt = "[hidden: %s]"; } else { fmt = "[hidden]"; } } g_string_append_printf(s, fmt, text); g_free(text); } GSList *l = NULL; for (l = media; l; l = l->next) { // TODO maybe support hiding media when it's marked NSFW. // (note that only media is hidden when it's marked NSFW. the text still shows.) // (note that we already don't show media, since this is all text, but IRC clients might.) char *url = l->data; if ((text_value && strstr(text_value->u.string.ptr, url)) || strstr(s->str, url)) { // skip URLs already in the text continue; } if (s->len) { g_string_append(s, " "); } g_string_append(s, url); } ms->text = g_string_free(s, FALSE); // we keep the data } g_slist_free(media); // elements are pointers into node and don't need to be freed if (ms->text && ms->account && ms->id) { return ms; } ms_free(ms); return NULL; } /** * Function to fill a mastodon_notification struct. */ static struct mastodon_notification *mastodon_xt_get_notification(const json_value *node, struct im_connection *ic) { if (node->type != json_object) { return FALSE; } struct mastodon_notification *mn = g_new0(struct mastodon_notification, 1); JSON_O_FOREACH(node, k, v) { if (strcmp("id", k) == 0) { mn->id = mastodon_json_int64(v); } else if (strcmp("created_at", k) == 0 && v->type == json_string) { struct tm parsed; /* Very sensitive to changes to the formatting of this field. :-( Also assumes the timezone used is UTC since C time handling functions suck. */ if (strptime(v->u.string.ptr, MASTODON_TIME_FORMAT, &parsed) != NULL) { mn->created_at = mktime_utc(&parsed); } } else if (strcmp("account", k) == 0 && v->type == json_object) { mn->account = mastodon_xt_get_user(v); } else if (strcmp("status", k) == 0 && v->type == json_object) { mn->status = mastodon_xt_get_status(v, ic); } else if (strcmp("type", k) == 0 && v->type == json_string) { if (strcmp(v->u.string.ptr, "mention") == 0) { mn->type = MN_MENTION; } else if (strcmp(v->u.string.ptr, "reblog") == 0) { mn->type = MN_REBLOG; } else if (strcmp(v->u.string.ptr, "favourite") == 0) { mn->type = MN_FAVOURITE; } else if (strcmp(v->u.string.ptr, "follow") == 0) { mn->type = MN_FOLLOW; } } } if (mn->type) { return mn; } mn_free(mn); return NULL; } static gboolean mastodon_xt_get_status_list(struct im_connection *ic, const json_value *node, struct mastodon_list *ml) { ml->type = ML_STATUS; if (node->type != json_array) { return FALSE; } int i; for (i = 0; i < node->u.array.length; i++) { struct mastodon_status *ms = mastodon_xt_get_status(node->u.array.values[i], ic); if (ms) { /* Code that calls this will display the toots in the home timeline, i.e. the account channel. This is true * right after a login and when displaying search results or a toot context. */ ms->subscription = MT_HOME; ml->list = g_slist_prepend(ml->list, ms); } } ml->list = g_slist_reverse(ml->list); return TRUE; } static gboolean mastodon_xt_get_notification_list(struct im_connection *ic, const json_value *node, struct mastodon_list *ml) { ml->type = ML_NOTIFICATION; if (node->type != json_array) { return FALSE; } int i; for (i = 0; i < node->u.array.length; i++) { struct mastodon_notification *mn = mastodon_xt_get_notification(node->u.array.values[i], ic); if (mn) { ml->list = g_slist_prepend(ml->list, mn); } } ml->list = g_slist_reverse(ml->list); return TRUE; } /* Will log messages either way. Need to keep track of IDs for stream deduping. Plus, show_ids is on by default and I don't see why anyone would disable it. */ static char *mastodon_msg_add_id(struct im_connection *ic, struct mastodon_status *ms, const char *prefix) { struct mastodon_data *md = ic->proto_data; int reply_to = -1; int idx = -1; /* See if we know this status and if we know the status this one is replying to. */ int i; for (i = 0; i < MASTODON_LOG_LENGTH; i++) { if (ms->reply_to && md->log[i].id == ms->reply_to) { reply_to = i; } if (md->log[i].id == ms->id) { idx = i; } if (idx != -1 && (!ms->reply_to || reply_to != -1)) { break; } } /* If we didn't find the status, it's new and needs an id, and we want to record who said it, and when they said * it, and who they mentioned, and the spoiler they used. We need to do this in two places: the md->log, and per * user in the mastodon_user_data (mud). */ if (idx == -1) { idx = md->log_id = (md->log_id + 1) % MASTODON_LOG_LENGTH; md->log[idx].id = ms->id; md->log[idx].visibility = ms->visibility; g_slist_free_full(md->log[idx].mentions, g_free); md->log[idx].mentions = g_slist_copy_deep(ms->mentions, (GCopyFunc) ma_copy, NULL); g_free(md->log[idx].spoiler_text); md->log[idx].spoiler_text = g_strdup(ms->spoiler_text); // no problem if NULL gint64 id = set_getint(&ic->acc->set, "account_id"); if (ms->account->id == id) { /* If this is our own status, use a fake bu without data since we can't be found by handle. This * will allow us to reply to our own messages, for example. */ md->log[idx].bu = &mastodon_log_local_user; } else { bee_user_t *bu = bee_user_by_handle(ic->bee, ic, ms->account->acct); struct mastodon_user_data *mud = bu->data; if (ms->id > mud->last_id) { mud->last_id = ms->id; mud->last_time = ms->created_at; mud->visibility = ms->visibility; g_slist_free_full(mud->mentions, (GDestroyNotify) ma_free); mud->mentions = g_slist_copy_deep(ms->mentions, (GCopyFunc) ma_copy, NULL); g_free(mud->spoiler_text); mud->spoiler_text = g_strdup(ms->spoiler_text); // no problem if NULL } md->log[idx].bu = bu; } } if (set_getbool(&ic->acc->set, "show_ids")) { if (reply_to != -1) { return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s", idx, reply_to, prefix, ms->text); } else { return g_strdup_printf("\002[\002%02x\002]\002 %s%s", idx, prefix, ms->text); } } else { if (*prefix) { return g_strconcat(prefix, ms->text, NULL); } else { return NULL; } } } /** * Helper function for mastodon_status_show_chat. */ static void mastodon_status_show_chat1(struct im_connection *ic, gboolean me, struct groupchat *c, char *msg, struct mastodon_status *ms) { if (me) { mastodon_visibility_t default_visibility = mastodon_default_visibility(ic); if (ms->visibility == default_visibility) { imcb_chat_log(c, "You: %s", msg ? msg : ms->text); } else { imcb_chat_log(c, "You, %s: %s", mastodon_visibility(ms->visibility), msg ? msg : ms->text); } } else { imcb_chat_msg(c, ms->account->acct, msg ? msg : ms->text, 0, ms->created_at); } } /** * Function that is called to see the statuses in a group chat. If the user created appropriate group chats (see setup * in mastodon_chat_join()), then we have extra streams providing the toots for these streams. The subscription * attribute gives us a basic hint of how the status wants to be sorted. Now, we also have a TIMELINE command, which * allows us to simulate the result. In this case, we can't be sure that appropriate group chats exist and thus we need * to put those statuses into the user timeline if they do not. */ static void mastodon_status_show_chat(struct im_connection *ic, struct mastodon_status *status) { gint64 id = set_getint(&ic->acc->set, "account_id"); gboolean me = (status->account->id == id); if (!me) { /* MUST be done before mastodon_msg_add_id() to avoid #872. */ mastodon_add_buddy(ic, status->account->id, status->account->acct, status->account->display_name); } char *msg = mastodon_msg_add_id(ic, status, ""); gboolean seen = FALSE; struct mastodon_user_data *mud; struct groupchat *c; bee_user_t *bu; GSList *l; switch (status->subscription) { case MT_LIST: /* Add the status to existing group chats with a topic matching any the lists this user is part of. */ bu = bee_user_by_handle(ic->bee, ic, status->account->acct); mud = (struct mastodon_user_data*) bu->data; for (l = mud->lists; l; l = l->next) { char *title = l->data; struct groupchat *c = bee_chat_by_title(ic->bee, ic, title); if (c) { mastodon_status_show_chat1(ic, me, c, msg, status); seen = TRUE; } } break; case MT_HASHTAG: /* Add the status to any other existing group chats whose title matches one of the tags, including the hash! */ for (l = status->tags; l; l = l->next) { char *tag = l->data; char *title = g_strdup_printf("#%s", tag); struct groupchat *c = bee_chat_by_title(ic->bee, ic, title); if (c) { mastodon_status_show_chat1(ic, me, c, msg, status); seen = TRUE; } g_free(title); } break; case MT_LOCAL: /* If there is an appropriate group chat, do not put it in the user timeline. */ c = bee_chat_by_title(ic->bee, ic, "local"); if (c) { mastodon_status_show_chat1(ic, me, c, msg, status); seen = TRUE; } break; case MT_FEDERATED: /* If there is an appropriate group chat, do not put it in the user timeline. */ c = bee_chat_by_title(ic->bee, ic, "federated"); if (c) { mastodon_status_show_chat1(ic, me, c, msg, status); seen = TRUE; } break; case MT_HOME: /* This is the default */ break; } if (!seen) { c = mastodon_groupchat_init(ic); mastodon_status_show_chat1(ic, me, c, msg, status); } g_free(msg); } /** * Function that is called to see statuses as private messages. */ static void mastodon_status_show_msg(struct im_connection *ic, struct mastodon_status *ms) { struct mastodon_data *md = ic->proto_data; char from[MAX_STRING] = ""; char *text = NULL; gint64 id = set_getint(&ic->acc->set, "account_id"); gboolean me = (ms->account->id == id); char *name = set_getstr(&ic->acc->set, "name"); if (md->flags & MASTODON_MODE_ONE) { char *prefix = g_strdup_printf("\002<\002%s\002>\002 ", ms->account->acct); text = mastodon_msg_add_id(ic, ms, prefix); /* may return NULL */ g_free(prefix); g_strlcpy(from, name, sizeof(from)); imcb_buddy_msg(ic, from, text ? text : ms->text, 0, ms->created_at); } else if (!me) { mastodon_add_buddy(ic, ms->account->id, ms->account->acct, ms->account->display_name); text = mastodon_msg_add_id(ic, ms, ""); /* may return NULL */ imcb_buddy_msg(ic, *from ? from : ms->account->acct, text ? text : ms->text, 0, ms->created_at); } else if (!ms->mentions) { text = mastodon_msg_add_id(ic, ms, "You, direct, but without mentioning anybody: "); /* may return NULL */ mastodon_log(ic, text ? text : ms->text); } else { text = mastodon_msg_add_id(ic, ms, "You, direct: "); /* At this point we have to cheat: if this is the echo of a message we're sending, we still want this message to * show up in the query buffer where we're chatting with somebody. So even though it is "from us" we're going to * fake that it is "from the recipient". And worse: we want to do this for all the buddies if we're chatting * directly with multiple people. */ GSList *l; for (l = ms->mentions; l; l = g_slist_next(l)) { bee_user_t *bu; struct mastodon_account *ma = (struct mastodon_account *) l->data; if ((bu = bee_user_by_handle(ic->bee, ic, ma->acct))) { mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name); imcb_buddy_msg(ic, ma->acct, text ? text : ms->text, 0, ms->created_at); } } } g_free(text); } struct mastodon_status *mastodon_notification_to_status(struct mastodon_notification *notification) { struct mastodon_account *ma = notification->account; struct mastodon_status *ms = notification->status; if (ma == NULL) { // Should not happen. ma = g_new0(struct mastodon_account, 1); ma->acct = g_strdup("anon"); ma->display_name = g_strdup("Unknown"); } /* The status in the notification was written by you, it's account is your account, but now somebody else is doing * something with it. We want to avoid the extra You at the beginning, "You: [01] @foo boosted your status: bla" * should be "<foo> [01] boosted your status: bla" or "<foo> followed you". So we're creating a fake status with a * copy of the notification account. */ if (ms == NULL) { /* Could be a FOLLOW notification without status. */ ms = g_new0(struct mastodon_status, 1); ms->account = ma_copy(notification->account); ms->created_at = notification->created_at; /* This ensures that ms will be freed when the notification is freed. */ notification->status = ms; } else { /* Adopt the account from the notification. The account will be freed when the notification frees the status. */ ma_free(ms->account); ms->account = ma; notification->account = NULL; } /* Make sure filters from the notification context know that this status is from a notification. */ ms->is_notification = TRUE; char *original = ms->text; switch (notification->type) { case MN_MENTION: // this is fine original = NULL; break; case MN_REBLOG: ms->text = g_strdup_printf("boosted your status: %s", original); break; case MN_FAVOURITE: ms->text = g_strdup_printf("favourited your status: %s", original); break; case MN_FOLLOW: ms->text = g_strdup_printf("[%s] followed you", ma->display_name); break; } g_free(original); return ms; } /** * Test whether a filter applies to the text. */ gboolean mastodon_filter_matches_it(char *text, struct mastodon_filter *mf) { if (!text) return FALSE; if (!mf->whole_word) { return strstr(text, mf->phrase_case_folded) != NULL; } else { /* Find the character at the beginning of the phrase and the character at the end of the phrase. */ int len = strlen(mf->phrase_case_folded); gunichar p1 = g_utf8_get_char(mf->phrase_case_folded); gunichar p2 = g_utf8_get_char(g_utf8_prev_char(mf->phrase_case_folded + len)); gboolean p1_is_alnum = g_unichar_isalnum(p1); gboolean p2_is_alnum = g_unichar_isalnum(p2); /* Start searching from the beginning. When we continue searching because a match is not at word boundaries, just skip a single character because matches can overlap. */ gchar *s = text; while ((s = strstr (s, mf->phrase_case_folded))) { /* At the beginning of the text counts as a word boundary. If the beginning of the phrase is not * alphanumeric, we don't care about word boundaries. */ if (s != text && p1_is_alnum) { /* Find the character before the match. This could potentially be the first character. */ gunichar c = g_utf8_get_char(g_utf8_prev_char(s)); /* If this is also an alphanumeric character, then this is not a word boundary. */ if (g_unichar_isalnum(c)) { s = g_utf8_next_char(s); continue; } } /* If the beginning of the phrase is not alphanumeric, we don't care about word boundaries. */ if (p2_is_alnum) { /* Find the character after the match. This could potentially be the zero byte. */ gunichar c = g_utf8_get_char(g_utf8_prev_char(s) + len); /* If this is the end of the string, or an alphanumeric character, then this is not a word boundary. */ if (!c || g_unichar_isalnum(c)) { s = g_utf8_next_char(s); continue; } } /* Otherwise we're golden. */ return TRUE; } return FALSE; } } /** * Test whether a filter applies to the status. */ gboolean mastodon_filter_matches(struct mastodon_status *ms, struct mastodon_filter *mf) { if (!ms || !mf || !mf->phrase_case_folded) return FALSE; return (mastodon_filter_matches_it(ms->content_case_folded, mf) || mastodon_filter_matches_it(ms->spoiler_text_case_folded, mf)); } /** * Show the status to the user. */ static void mastodon_status_show(struct im_connection *ic, struct mastodon_status *ms) { struct mastodon_data *md = ic->proto_data; if (ms->account == NULL || ms->text == NULL) { return; } /* Must check all the filters. */ GSList *l; for (l = md->filters; l; l = g_slist_next(l)) { struct mastodon_filter *mf = (struct mastodon_filter *) l->data; /* MF_HOME filter applies to the home timeline, MF_PUBLIC applies to the local and federated public timelines, * MF_NOTIFICATION applies to any notifications received. */ if (((mf->context & MF_HOME && ms->subscription == MT_HOME) || (mf->context & MF_PUBLIC && (ms->subscription == MT_LOCAL || ms->subscription == MT_FEDERATED)) || (mf->context & MF_NOTIFICATIONS && ms->is_notification) || mf->context & MF_THREAD) && mastodon_filter_matches(ms, mf)) { /* Do not show. */ return; } } /* Deduplicating only affects the previous status shown. Thus, if we got mentioned in a toot by a user that we're * following, chances are that both events will arrive in sequence. In this case, the second one will be skipped. * This will also work when flushing timelines after connecting: notification and status update should be close to * each other. This will fail if the stream is really busy. Critically, it won't suppress statuses from later * context and timeline requests. */ if (ms->id == md->seen_id) { return; } else { md->seen_id = ms->id; } /* Grrrr. Would like to do this during parsing, but can't access settings from there. */ if (set_getbool(&ic->acc->set, "strip_newlines")) { strip_newlines(ms->text); } /* By default, everything except direct messages goes into a channel. */ if (md->flags & MASTODON_MODE_CHAT && ms->visibility != MV_DIRECT) { mastodon_status_show_chat(ic, ms); } else { mastodon_status_show_msg(ic, ms); } } static void mastodon_notification_show(struct im_connection *ic, struct mastodon_notification *notification) { gboolean show = TRUE; switch (notification->type) { case MN_MENTION: show = !set_getbool(&ic->acc->set, "hide_mentions"); break; case MN_REBLOG: show = !set_getbool(&ic->acc->set, "hide_boosts"); break; case MN_FAVOURITE: show = !set_getbool(&ic->acc->set, "hide_favourites"); break; case MN_FOLLOW: show = !set_getbool(&ic->acc->set, "hide_follows"); break; } if (show) mastodon_status_show(ic, mastodon_notification_to_status(notification)); } /** * Add exactly one notification to the timeline. */ static void mastodon_stream_handle_notification(struct im_connection *ic, json_value *parsed, mastodon_timeline_type_t subscription) { struct mastodon_notification *mn = mastodon_xt_get_notification(parsed, ic); if (mn) { /* A follow notification has no status and thus cannot be assigned a subsription (see mastodon_timeline_type_t). * But if there is a status associated with the notification, we know where it came from. */ if (mn->status) mn->status->subscription = subscription; mastodon_notification_show(ic, mn); mn_free(mn); } } /** * Add exactly one status to the timeline. */ static void mastodon_stream_handle_update(struct im_connection *ic, json_value *parsed, mastodon_timeline_type_t subscription) { struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic); if (ms) { ms->subscription = subscription; mastodon_status_show(ic, ms); ms_free(ms); } } /* Let the user know if a status they have recently seen was deleted. If we can't find the deleted status in our list of * recently seen statuses, ignore the event. */ static void mastodon_stream_handle_delete(struct im_connection *ic, json_value *parsed) { struct mastodon_data *md = ic->proto_data; guint64 id = mastodon_json_int64(parsed); if (id) { int i; for (i = 0; i < MASTODON_LOG_LENGTH; i++) { if (md->log[i].id == id) { mastodon_log(ic, "Status %02x was deleted.", i); md->log[i].id = 0; // prevent future references return; } } } else { mastodon_log(ic, "Error parsing a deletion event."); } } static void mastodon_stream_handle_event(struct im_connection *ic, mastodon_evt_flags_t evt_type, json_value *parsed, mastodon_timeline_type_t subscription) { if (evt_type == MASTODON_EVT_UPDATE) { mastodon_stream_handle_update(ic, parsed, subscription); } else if (evt_type == MASTODON_EVT_NOTIFICATION) { mastodon_stream_handle_notification(ic, parsed, subscription); } else if (evt_type == MASTODON_EVT_DELETE) { mastodon_stream_handle_delete(ic, parsed); } else { mastodon_log(ic, "Ignoring event type %d", evt_type); } } /** * When streaming, we also want to tag the events appropriately. This only affects updates, for now. */ static void mastodon_http_stream(struct http_request *req, mastodon_timeline_type_t subscription) { struct im_connection *ic = req->data; struct mastodon_data *md = ic->proto_data; int len = 0; char *nl; if (!g_slist_find(mastodon_connections, ic)) { return; } if ((req->flags & HTTPC_EOF) || !req->reply_body) { md->streams = g_slist_remove (md->streams, req); imcb_error(ic, "Stream closed (%s)", req->status_string); imc_logout(ic, TRUE); return; } /* It doesn't matter which stream sent us something. */ ic->flags |= OPT_PONGED; /* https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format */ if (req->reply_body[0] == ':' && (nl = strchr(req->reply_body, '\n'))) { // found a comment such as the heartbeat ":thump\n" len = nl - req->reply_body + 1; goto end; } else if (!(nl = strstr(req->reply_body, "\n\n"))) { // wait until we have a complete event return; } // include the two newlines at the end len = nl - req->reply_body + 2; if (len > 0) { char *p; mastodon_evt_flags_t evt_type = MASTODON_EVT_UNKNOWN; // assuming space after colon if (strncmp(req->reply_body, "event: ", 7) == 0) { p = req->reply_body + 7; if (strncmp(p, "update\n", 7) == 0) { evt_type = MASTODON_EVT_UPDATE; p += 7; } else if (strncmp(p, "notification\n", 13) == 0) { evt_type = MASTODON_EVT_NOTIFICATION; p += 13; } else if (strncmp(p, "delete\n", 7) == 0) { evt_type = MASTODON_EVT_DELETE; p += 7; } } if (evt_type != MASTODON_EVT_UNKNOWN) { GString *data = g_string_new(""); char* q; while (strncmp(p, "data: ", 6) == 0) { p += 6; q = strchr(p, '\n'); p[q-p] = '\0'; g_string_append(data, p); p = q + 1; } json_value *parsed; if ((parsed = json_parse(data->str, data->len))) { mastodon_stream_handle_event(ic, evt_type, parsed, subscription); json_value_free(parsed); } g_string_free(data, TRUE); } } end: http_flush_bytes(req, len); /* We might have multiple events */ if (req->body_size > 0) { mastodon_http_stream(req, subscription); } } static void mastodon_http_stream_user(struct http_request *req) { mastodon_http_stream(req, MT_HOME); } static void mastodon_http_stream_hashtag(struct http_request *req) { mastodon_http_stream(req, MT_HASHTAG); } static void mastodon_http_stream_local(struct http_request *req) { mastodon_http_stream(req, MT_LOCAL); } static void mastodon_http_stream_federated(struct http_request *req) { mastodon_http_stream(req, MT_FEDERATED); } static void mastodon_http_stream_list(struct http_request *req) { mastodon_http_stream(req, MT_LIST); } /** * Make sure a request continues stream instead of closing. */ void mastodon_stream(struct im_connection *ic, struct http_request *req) { struct mastodon_data *md = ic->proto_data; if (req) { req->flags |= HTTPC_STREAMING; md->streams = g_slist_prepend(md->streams, req); } } /** * Open the user (home) timeline. */ void mastodon_open_user_stream(struct im_connection *ic) { struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_USER_URL, mastodon_http_stream_user, ic, HTTP_GET, NULL, 0); mastodon_stream(ic, req); } /** * Open a stream for a hashtag timeline and return the request. */ struct http_request *mastodon_open_hashtag_stream(struct im_connection *ic, char *hashtag) { char *args[2] = { "tag", hashtag, }; struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_HASHTAG_URL, mastodon_http_stream_hashtag, ic, HTTP_GET, args, 2); mastodon_stream(ic, req); return req; } /** * Part two of the first callback: now we have mc->id. Now we're good to go. */ void mastodon_list_stream(struct im_connection *ic, struct mastodon_command *mc) { char *args[2] = { "list", g_strdup_printf("%" G_GINT64_FORMAT, mc->id), }; struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_LIST_URL, mastodon_http_stream_list, ic, HTTP_GET, args, 2); mastodon_stream(ic, req); /* We cannot return req here because this is a callback (as we had to figure out the list id before getting here). * This is why we must rely on the groupchat being part of mastodon_command (mc). */ struct groupchat *c = (struct groupchat *) mc->data; c->data = req; } /** * First callback to show the stream for a list. We need to parse the lists and find the one we're looking for, then * make our next request with the list id. */ static void mastodon_http_list_stream(struct http_request *req) { mastodon_chained_list(req, mastodon_list_stream); } void mastodon_open_unknown_list_stream(struct im_connection *ic, struct groupchat *c, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->data = (gpointer *) c; mc->str = g_strdup(title); mastodon_with_named_list(ic, mc, mastodon_http_list_stream); } /** * Open a stream for the local timeline and return the request. */ struct http_request *mastodon_open_local_stream(struct im_connection *ic) { struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_LOCAL_URL, mastodon_http_stream_local, ic, HTTP_GET, NULL, 0); mastodon_stream(ic, req); return req; } /** * Open a stream for the federated timeline and return the request. */ struct http_request *mastodon_open_federated_stream(struct im_connection *ic) { struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_FEDERATED_URL, mastodon_http_stream_federated, ic, HTTP_GET, NULL, 0); mastodon_stream(ic, req); return req; } /** * Look for the Link header and remember the URL to the next page of results. This will be used by the "more" command. */ static void mastodon_handle_header(struct http_request *req, mastodon_more_t more_type) { struct im_connection *ic = req->data; // remember the URL to fetch more if there is a header saying that there is more (URL in angled brackets) char *header = NULL; if ((header = get_rfc822_header(req->reply_headers, "Link", 0))) { char *url = NULL; gboolean next = FALSE; int i; for (i = 0; header[i]; i++) { if (header[i] == '<') { url = header + i + 1; } else if (url && header[i] == '>') { header[i] = 0; if (strncmp(header + i + 1, "; rel=\"next\"", 12) == 0) { next = TRUE; break; } else { url = NULL; } } } struct mastodon_data *md = ic->proto_data; g_free(md->next_url); md->next_url = NULL; if (next) md->next_url = g_strdup(url); md->more_type = more_type; g_free(header); } } /** * Handle a request whose response contains nothing but statuses. Note that we expect req->data to be an im_connection, * not a mastodon_command (or NULL). */ static void mastodon_http_timeline(struct http_request *req, mastodon_timeline_type_t subscription) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "No statuses found in this timeline."); goto finish; } mastodon_handle_header(req, MASTODON_MORE_STATUSES); // Show in reverse order! int i; for (i = parsed->u.array.length - 1; i >= 0 ; i--) { json_value *node = parsed->u.array.values[i]; struct mastodon_status *ms = mastodon_xt_get_status(node, ic); if (ms) { ms->subscription = subscription; mastodon_status_show(ic, ms); ms_free(ms); } } finish: json_value_free(parsed); } static void mastodon_http_hashtag_timeline(struct http_request *req) { mastodon_http_timeline(req, MT_HASHTAG); } void mastodon_hashtag_timeline(struct im_connection *ic, char *hashtag) { char *url = g_strdup_printf(MASTODON_HASHTAG_TIMELINE_URL, hashtag); mastodon_http(ic, url, mastodon_http_hashtag_timeline, ic, HTTP_GET, NULL, 0); g_free(url); } static void mastodon_http_home_timeline(struct http_request *req) { mastodon_http_timeline(req, MT_HOME); } void mastodon_home_timeline(struct im_connection *ic) { mastodon_http(ic, MASTODON_HOME_TIMELINE_URL, mastodon_http_home_timeline, ic, HTTP_GET, NULL, 0); } static void mastodon_http_local_timeline(struct http_request *req) { mastodon_http_timeline(req, MT_LOCAL); } void mastodon_local_timeline(struct im_connection *ic) { char *args[2] = { "local", "1", }; mastodon_http(ic, MASTODON_PUBLIC_TIMELINE_URL, mastodon_http_local_timeline, ic, HTTP_GET, args, 2); } static void mastodon_http_federated_timeline(struct http_request *req) { mastodon_http_timeline(req, MT_FEDERATED); } void mastodon_federated_timeline(struct im_connection *ic) { mastodon_http(ic, MASTODON_PUBLIC_TIMELINE_URL, mastodon_http_federated_timeline, ic, HTTP_GET, NULL, 0); } /** * Second callback to show the timeline for a list. We finally got a list of statuses. */ static void mastodon_http_list_timeline2(struct http_request *req) { /* We have used a mastodon_command (mc) all this time, but now it's time to forget about it and use an im_connection * (ic) instead. */ struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; req->data = ic; mc_free(mc); mastodon_http_timeline(req, MT_LIST); } /** * Part two of the first callback to show the timeline for a list. In mc->id we have our list id. */ void mastodon_list_timeline(struct im_connection *ic, struct mastodon_command *mc) { char *url = g_strdup_printf(MASTODON_LIST_TIMELINE_URL, mc->id); mastodon_http(ic, url, mastodon_http_list_timeline2, mc, HTTP_GET, NULL, 0); g_free(url); } /** * First callback to show the timeline for a list. We need to parse the lists and find the one we're looking for, then * make our next request with the list id. */ static void mastodon_http_list_timeline(struct http_request *req) { mastodon_chained_list(req, mastodon_list_timeline); } /** * Timeline for a named list. This requires two callbacks: the first to find the list id, the second one to do the * actual work. */ void mastodon_unknown_list_timeline(struct im_connection *ic, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->str = g_strdup(title); mastodon_with_named_list(ic, mc, mastodon_http_list_timeline); } /** * Call this one after receiving timeline/notifications. Show to user * once we have both. */ void mastodon_flush_timeline(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; struct mastodon_list *home_timeline; struct mastodon_list *notifications; GSList *output = NULL; GSList *l; if (md == NULL) { return; } imcb_connected(ic); /* Wait until we have all the data we need. */ if (!(md->flags & MASTODON_GOT_TIMELINE) || !(md->flags & MASTODON_GOT_NOTIFICATIONS) || !(md->flags & MASTODON_GOT_FILTERS)) { return; } home_timeline = md->home_timeline_obj; notifications = md->notifications_obj; if (home_timeline && home_timeline->list) { for (l = home_timeline->list; l; l = g_slist_next(l)) { output = g_slist_insert_sorted(output, l->data, mastodon_compare_elements); } } if (notifications && notifications->list) { for (l = notifications->list; l; l = g_slist_next(l)) { // Skip notifications older than the earliest entry in the timeline. struct mastodon_status *ms = mastodon_notification_to_status((struct mastodon_notification *) l->data); if (output && mastodon_compare_elements(ms, output->data) < 0) { continue; } output = g_slist_insert_sorted(output, ms, mastodon_compare_elements); } } while (output) { struct mastodon_status *ms = output->data; mastodon_status_show(ic, ms); output = g_slist_remove(output, ms); } ml_free(home_timeline); ml_free(notifications); g_slist_free(output); md->flags &= ~(MASTODON_GOT_TIMELINE | MASTODON_GOT_NOTIFICATIONS | MASTODON_GOT_FILTERS); md->home_timeline_obj = md->notifications_obj = NULL; } /** * Callback for getting the home timeline. This runs in parallel to * getting the notifications. */ static void mastodon_http_get_home_timeline(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } struct mastodon_data *md = ic->proto_data; json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_list *ml = g_new0(struct mastodon_list, 1); mastodon_xt_get_status_list(ic, parsed, ml); json_value_free(parsed); md->home_timeline_obj = ml; md->flags |= MASTODON_GOT_TIMELINE; mastodon_flush_timeline(ic); } /** * Callback for getting the notifications. This runs in parallel to * getting the home timeline. */ static void mastodon_http_get_notifications(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } struct mastodon_data *md = ic->proto_data; json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_list *ml = g_new0(struct mastodon_list, 1); mastodon_xt_get_notification_list(ic, parsed, ml); json_value_free(parsed); md->notifications_obj = ml; md->flags |= MASTODON_GOT_NOTIFICATIONS; mastodon_flush_timeline(ic); } /** * See mastodon_initial_timeline. */ static void mastodon_get_home_timeline(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; ml_free(md->home_timeline_obj); md->home_timeline_obj = NULL; md->flags &= ~MASTODON_GOT_TIMELINE; mastodon_http(ic, MASTODON_HOME_TIMELINE_URL, mastodon_http_get_home_timeline, ic, HTTP_GET, NULL, 0); } /** * See mastodon_initial_timeline. */ static void mastodon_get_notifications(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; ml_free(md->notifications_obj); md->notifications_obj = NULL; md->flags &= ~MASTODON_GOT_NOTIFICATIONS; mastodon_http(ic, MASTODON_NOTIFICATIONS_URL, mastodon_http_get_notifications, ic, HTTP_GET, NULL, 0); } static void mastodon_get_filters(struct im_connection *ic); /** * Get the initial timeline. This consists of three things: the home timeline, notifications, and filters. During normal * use, the timeline and the notifications are provided via the Streaming API. However, when we connect to an instance * we want to load the home timeline and notifications and sort them in a meaningful way. We use flags: * MASTODON_GOT_TIMELINE to indicate that we now have home timeline, MASTODON_GOT_NOTIFICATIONS to indicate that we now * have notifications, and MASTODON_GOT_FILTERS to indicate that we now have filters . All callbacks will attempt to * flush the initial timeline, but this will only succeed if all three flags are set. */ void mastodon_initial_timeline(struct im_connection *ic) { imcb_log(ic, "Getting home timeline"); mastodon_get_home_timeline(ic); mastodon_get_notifications(ic); mastodon_get_filters(ic); return; } /** * Callback for getting notifications manually. */ static void mastodon_http_notifications(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "No notifications found."); goto finish; } mastodon_handle_header(req, MASTODON_MORE_NOTIFICATIONS); // Show in reverse order! int i; for (i = parsed->u.array.length - 1; i >= 0 ; i--) { json_value *node = parsed->u.array.values[i]; struct mastodon_notification *mn = mastodon_xt_get_notification(node, ic); if (mn) { mastodon_notification_show(ic, mn); mn_free(mn); } } finish: json_value_free(parsed); } /** * Notifications are usually shown by the Streaming API, and when showing the initial timeline after connecting. In * order to allow manual review (and going through past notifications using the more command, we need yet another way to * get notifications. */ void mastodon_notifications(struct im_connection *ic) { mastodon_http(ic, MASTODON_NOTIFICATIONS_URL, mastodon_http_notifications, ic, HTTP_GET, NULL, 0); } mastodon_visibility_t mastodon_default_visibility(struct im_connection *ic) { return mastodon_parse_visibility(set_getstr(&ic->acc->set, "visibility")); } char *mastodon_visibility(mastodon_visibility_t visibility) { switch (visibility) { case MV_UNKNOWN: case MV_PUBLIC: return "public"; case MV_UNLISTED: return "unlisted"; case MV_PRIVATE: return "private"; case MV_DIRECT: return "direct"; } g_assert(FALSE); // should not happen return NULL; } /** * Generic callback to use after sending a POST request to mastodon when the reply doesn't have any information we need. * All we care about are errors. If got here, there was no error. If you want to tell the user that everything went * fine, call mastodon_http_callback_and_ack instead. This command also stores some information for later use. */ static void mastodon_http_callback(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } /* Store stuff in the undo/redo stack. */ struct mastodon_data *md = ic->proto_data; md->last_id = 0; struct mastodon_status *ms; switch (mc->command) { case MC_UNKNOWN: break; case MC_POST: ms = mastodon_xt_get_status(parsed, ic); gint64 id = set_getint(&ic->acc->set, "account_id"); if (ms && ms->id && ms->account->id == id) { /* we posted this status */ md->last_id = ms->id; md->last_visibility = ms->visibility; g_free(md->last_spoiler_text); md->last_spoiler_text = ms->spoiler_text; // adopt ms->spoiler_text = NULL; g_slist_free_full(md->mentions, (GDestroyNotify) ma_free); md->mentions = ms->mentions; // adopt ms->mentions = NULL; if(md->undo_type == MASTODON_NEW) { GString *todo = g_string_new (NULL); char *undo = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id); /* At this point redoing the reply no longer has the reference to the toot we are replying to (which * only works by looking it up in the mastodon_user_data (mud) or the md->log). That is why we need * to add spoiler_text and visibility to the todo item on our redo list. */ if (ms->spoiler_text) { g_string_append_printf(todo, "cw %s" FS, ms->spoiler_text); } else { g_string_append(todo, "cw" FS); } if (mastodon_default_visibility(ic) != ms->visibility) { g_string_append_printf(todo, "visibility %s" FS, mastodon_visibility(ms->visibility)); } else { g_string_append(todo, "visibility" FS); } if (ms->reply_to) { g_string_append_printf(todo, "reply %" G_GUINT64_FORMAT " ", ms->reply_to); } else { g_string_append(todo, "post "); } g_string_append(todo, ms->content); mastodon_do(ic, todo->str, undo); g_string_free(todo, FALSE); /* data is kept by mastodon_do! */ } else { char *s = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id); mastodon_do_update(ic, s); g_free(s); } } break; case MC_FOLLOW: case MC_UNFOLLOW: case MC_BLOCK: case MC_UNBLOCK: case MC_FAVOURITE: case MC_UNFAVOURITE: case MC_PIN: case MC_UNPIN: case MC_ACCOUNT_MUTE: case MC_ACCOUNT_UNMUTE: case MC_STATUS_MUTE: case MC_STATUS_UNMUTE: case MC_BOOST: case MC_UNBOOST: case MC_LIST_CREATE: case MC_LIST_DELETE: case MC_LIST_ADD_ACCOUNT: case MC_LIST_REMOVE_ACCOUNT: case MC_FILTER_CREATE: case MC_FILTER_DELETE: case MC_DELETE: md->last_id = 0; mastodon_do(ic, mc->redo, mc->undo); // adopting these strings: do not free them at the end mc->redo = mc->undo = 0; break; } mc_free(mc); json_value_free(parsed); } /** * Call the generic callback function and print an acknowledgement for the user. * Commands should use mastodon_post instead. */ static void mastodon_http_callback_and_ack(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; mastodon_http_callback(req); // this frees mc if (req->status_code == 200) { mastodon_log(ic, "Command processed successfully"); } } /** * Return a static string n spaces long. No deallocation needed. */ static char *indent(int n) { char *spaces = " "; int len = 10; return n > len ? spaces : spaces + len - n; } /** * Return a static yes or no string. No deallocation needed. */ static char *yes_or_no(int bool) { return bool ? "yes" : "no"; } /** * Log a JSON array out to the channel. When you call it, use a * prefix of 0. Recursive calls will then indent nested objects. */ static void mastodon_log_array(struct im_connection *ic, json_value *node, int prefix) { int i; for (i = 0; i < node->u.array.length; i++) { json_value *v = node->u.array.values[i]; char *s; switch (v->type) { case json_object: if (v->u.object.values == 0) { mastodon_log(ic, "%s{}", indent(prefix)); break; } mastodon_log(ic, "%s{", indent(prefix)); mastodon_log_object (ic, v, prefix + 1); mastodon_log(ic, "%s}", indent(prefix)); break; case json_array: if (v->u.array.length == 0) { mastodon_log(ic, "%s[]", indent(prefix)); break; } mastodon_log(ic, "%s[", indent(prefix)); int i; for (i = 0; i < v->u.array.length; i++) { mastodon_log_object (ic, node->u.array.values[i], prefix + 1); } mastodon_log(ic, "%s]", indent(prefix)); break; case json_string: s = g_strdup(v->u.string.ptr); mastodon_strip_html(s); mastodon_log(ic, "%s%s", indent(prefix), s); g_free(s); break; case json_double: mastodon_log(ic, "%s%f", indent(prefix), v->u.dbl); break; case json_integer: mastodon_log(ic, "%s%d", indent(prefix), v->u.boolean); break; case json_boolean: mastodon_log(ic, "%s%s: %s", indent(prefix), yes_or_no(v->u.boolean)); break; case json_null: mastodon_log(ic, "%snull", indent(prefix)); break; case json_none: mastodon_log(ic, "%snone", indent(prefix)); break; } } } /** * Log a JSON object out to the channel. When you call it, use a * prefix of 0. Recursive calls will then indent nested objects. */ static void mastodon_log_object(struct im_connection *ic, json_value *node, int prefix) { char *s; JSON_O_FOREACH(node, k, v) { switch (v->type) { case json_object: if (v->u.object.values == 0) { mastodon_log(ic, "%s%s: {}", indent(prefix), k); break; } mastodon_log(ic, "%s%s: {", indent(prefix), k); mastodon_log_object (ic, v, prefix + 1); mastodon_log(ic, "%s}", indent(prefix)); break; case json_array: if (v->u.array.length == 0) { mastodon_log(ic, "%s%s: []", indent(prefix), k); break; } mastodon_log(ic, "%s%s: [", indent(prefix), k); mastodon_log_array(ic, v, prefix + 1); mastodon_log(ic, "%s]", indent(prefix)); break; case json_string: s = g_strdup(v->u.string.ptr); mastodon_strip_html(s); mastodon_log(ic, "%s%s: %s", indent(prefix), k, s); g_free(s); break; case json_double: mastodon_log(ic, "%s%s: %f", indent(prefix), k, v->u.dbl); break; case json_integer: mastodon_log(ic, "%s%s: %d", indent(prefix), k, v->u.boolean); break; case json_boolean: mastodon_log(ic, "%s%s: %s", indent(prefix), k, yes_or_no(v->u.boolean)); break; case json_null: mastodon_log(ic, "%s%s: null", indent(prefix), k); break; case json_none: mastodon_log(ic, "%s%s: unknown type", indent(prefix), k); break; } } } /** * Generic callback which simply logs the JSON response to the * channel. */ static void mastodon_http_log_all(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type == json_object) { mastodon_log_object(ic, parsed, 0); } else if (parsed->type == json_array) { mastodon_log_array(ic, parsed, 0); } else { mastodon_log(ic, "Sadly, the response to this request is not a JSON object or array."); } json_value_free(parsed); } /** * Function to POST a new status to mastodon. */ void mastodon_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to, mastodon_visibility_t visibility, char *spoiler_text) { char *args[8] = { "status", msg, "visibility", mastodon_visibility(visibility), "spoiler_text", spoiler_text, "in_reply_to_id", g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to) }; int count = 8; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->command = MC_POST; if (!in_reply_to) { count -= 2; } if (!spoiler_text) { count -= 2; if (in_reply_to) { args[4] = args[6]; args[5] = args[7]; // we have 2 pointers to the in_reply_to_id string now, } } // No need to acknowledge the processing of a post: we will get notified. mastodon_http(ic, MASTODON_STATUS_POST_URL, mastodon_http_callback, mc, HTTP_POST, args, count); g_free(args[7]); // but we only free one of them! } /** * Generic POST request taking a numeric ID. The format string must contain one placeholder for the ID, like * "/accounts/%" G_GINT64_FORMAT "/mute". */ void mastodon_post(struct im_connection *ic, char *format, mastodon_command_type_t command, guint64 id) { struct mastodon_data *md = ic->proto_data; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; if (md->undo_type == MASTODON_NEW) { mc->command = command; switch (command) { case MC_UNKNOWN: case MC_POST: case MC_DELETE: case MC_LIST_CREATE: case MC_LIST_DELETE: case MC_LIST_ADD_ACCOUNT: case MC_LIST_REMOVE_ACCOUNT: case MC_FILTER_CREATE: case MC_FILTER_DELETE: /* These commands should not be calling mastodon_post. Instead, call mastodon_post_status or whatever else * is required. */ break; case MC_FOLLOW: mc->redo = g_strdup_printf("follow %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unfollow %" G_GUINT64_FORMAT, id); break; case MC_UNFOLLOW: mc->redo = g_strdup_printf("unfollow %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("follow %" G_GUINT64_FORMAT, id); break; case MC_BLOCK: mc->redo = g_strdup_printf("block %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unblock %" G_GUINT64_FORMAT, id); break; case MC_UNBLOCK: mc->redo = g_strdup_printf("unblock %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("block %" G_GUINT64_FORMAT, id); break; case MC_FAVOURITE: mc->redo = g_strdup_printf("favourite %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unfavourite %" G_GUINT64_FORMAT, id); break; case MC_UNFAVOURITE: mc->redo = g_strdup_printf("unfavourite %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("favourite %" G_GUINT64_FORMAT, id); break; case MC_PIN: mc->redo = g_strdup_printf("pin %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unpin %" G_GUINT64_FORMAT, id); break; case MC_UNPIN: mc->redo = g_strdup_printf("unpin %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("pin %" G_GUINT64_FORMAT, id); break; case MC_ACCOUNT_MUTE: mc->redo = g_strdup_printf("mute user %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unmute user %" G_GUINT64_FORMAT, id); break; case MC_ACCOUNT_UNMUTE: mc->redo = g_strdup_printf("unmute user %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("mute user %" G_GUINT64_FORMAT, id); break; case MC_STATUS_MUTE: mc->redo = g_strdup_printf("mute %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unmute %" G_GUINT64_FORMAT, id); break; case MC_STATUS_UNMUTE: mc->redo = g_strdup_printf("unmute %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("mute %" G_GUINT64_FORMAT, id); break; case MC_BOOST: mc->redo = g_strdup_printf("boost %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("unboost %" G_GUINT64_FORMAT, id); break; case MC_UNBOOST: mc->redo = g_strdup_printf("unboost %" G_GUINT64_FORMAT, id); mc->undo = g_strdup_printf("boost %" G_GUINT64_FORMAT, id); break; } } char *url = g_strdup_printf(format, id); mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_POST, NULL, 0); g_free(url); } void mastodon_http_status_delete(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } /* Maintain undo/redo list. */ struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic); struct mastodon_data *md = ic->proto_data; gint64 id = set_getint(&ic->acc->set, "account_id"); if (ms && ms->id && ms->account->id == id) { /* we deleted our own status */ md->last_id = ms->id; mc->redo = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id); GString *todo = g_string_new (NULL); if (ms->spoiler_text) { g_string_append_printf(todo, "cw %s" FS, ms->spoiler_text); } else { g_string_append(todo, "cw" FS); } if (mastodon_default_visibility(ic) != ms->visibility) { g_string_append_printf(todo, "visibility %s" FS, mastodon_visibility(ms->visibility)); } else { g_string_append(todo, "visibility" FS); } if (ms->reply_to) { g_string_append_printf(todo, "reply %" G_GUINT64_FORMAT " ", ms->reply_to); } else { g_string_append(todo, "post "); } g_string_append(todo, ms->content); mc->undo = todo->str; g_string_free(todo, FALSE); /* data is kept by mc! */ } char *url = g_strdup_printf(MASTODON_STATUS_URL, mc->id); // No need to acknowledge the processing of the delete: we will get notified. mastodon_http(ic, url, mastodon_http_callback, mc, HTTP_DELETE, NULL, 0); g_free(url); } /** * Helper for all functions that need to act on a status before they * can do anything else. Provide a function to use as a callback. This * callback will get the status back and will need to call * mastodon_xt_get_status and do something with it. */ void mastodon_with_status(struct mastodon_command *mc, guint64 id, http_input_function func) { char *url = g_strdup_printf(MASTODON_STATUS_URL, id); mastodon_http(mc->ic, url, func, mc, HTTP_GET, NULL, 0); g_free(url); } /** * Delete a status. In order to ensure that we can undo and redo this, * fetch the status to be deleted before actually deleting it. */ void mastodon_status_delete(struct im_connection *ic, guint64 id) { struct mastodon_data *md = ic->proto_data; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; if (md->undo_type == MASTODON_NEW) { mc->command = MC_DELETE; mc->id = id; mastodon_with_status(mc, id, mastodon_http_status_delete); } else { // Shortcut char *url = g_strdup_printf(MASTODON_STATUS_URL, id); // No need to acknowledge the processing of the delete: we will get notified. mastodon_http(ic, url, mastodon_http_callback, mc, HTTP_DELETE, NULL, 0); g_free(url); } } /** * Callback for reporting a user for sending spam. */ void mastodon_http_report(struct http_request *req) { struct mastodon_report *mr = req->data; struct im_connection *ic = mr->ic; if (!g_slist_find(mastodon_connections, ic)) { goto finally; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finally; } struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic); if (ms) { mr->account_id = ms->account->id; ms_free(ms); } else { mastodon_log(ic, "Error: could not fetch toot to report."); goto finish; } char *args[6] = { "account_id", g_strdup_printf("%" G_GUINT64_FORMAT, mr->account_id), "status_ids", g_strdup_printf("%" G_GUINT64_FORMAT, mr->status_id), // API allows an array, here "comment", mr->comment, }; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mastodon_http(ic, MASTODON_REPORT_URL, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 6); g_free(args[1]); g_free(args[3]); finish: ms_free(ms); json_value_free(parsed); finally: // The report structure was created by mastodon_report and has // to be freed under all circumstances. mr_free(mr); } /** * Report a user. Since all we have is the id of the offending status, * we need to retrieve the status, first. */ void mastodon_report(struct im_connection *ic, guint64 id, char *comment) { char *url = g_strdup_printf(MASTODON_STATUS_URL, id); struct mastodon_report *mr = g_new0(struct mastodon_report, 1); mr->ic = ic; mr->status_id = id; mr->comment = g_strdup(comment); mastodon_http(ic, url, mastodon_http_report, mr, HTTP_POST, NULL, 0); g_free(url); } /** * Callback for search. */ void mastodon_http_search(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } json_value *v; gboolean found = FALSE; /* hashtags */ if ((v = json_o_get(parsed, "hashtags")) && (v->type == json_array) && (v->u.array.length > 0)) { found = TRUE; int i; for (i = 0; i < v->u.array.length; i++) { json_value *s; s = v->u.array.values[i]; if (s->type == json_string) { mastodon_log(ic, "#%s", s->u.string.ptr); } } } /* accounts */ if ((v = json_o_get(parsed, "accounts")) && (v->type == json_array) && (v->u.array.length > 0)) { found = TRUE; int i; for (i = 0; i < v->u.array.length; i++) { json_value *a; a = v->u.array.values[i]; if ((a->type == json_object)) { mastodon_log(ic, "@%s %s", json_o_str(a, "acct"), json_o_str(a, "display_name")); } } } /* statuses */ if ((v = json_o_get(parsed, "statuses")) && (v->type == json_array) && (v->u.array.length > 0)) { found = TRUE; struct mastodon_list *ml = g_new0(struct mastodon_list, 1); mastodon_xt_get_status_list(ic, v, ml); GSList *l; for (l = ml->list; l; l = g_slist_next(l)) { struct mastodon_status *s = (struct mastodon_status *) l->data; mastodon_status_show_chat(ic, s); } ml_free(ml); } json_value_free(parsed); if (!found) { mastodon_log(ic, "Search returned no results on this instance"); } } /** * Search for a status URL, account, or hashtag. */ void mastodon_search(struct im_connection *ic, char *what) { char *args[2] = { "q", what, }; mastodon_http(ic, MASTODON_SEARCH_URL, mastodon_http_search, ic, HTTP_GET, args, 2); } /** * Show information about the instance. */ void mastodon_instance(struct im_connection *ic) { mastodon_http(ic, MASTODON_INSTANCE_URL, mastodon_http_log_all, ic, HTTP_GET, NULL, 0); } /** * Show information about an account. */ void mastodon_account(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id); mastodon_http(ic, url, mastodon_http_log_all, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Helper for all functions that need to search for an account before * they can do anything else. Provide a function to use as a callback. * This callback will get the account search result back and will need * to call mastodon_xt_get_user and do something with it. */ void mastodon_with_search_account(struct im_connection *ic, char *who, http_input_function func) { char *args[2] = { "q", who, }; mastodon_http(ic, MASTODON_ACCOUNT_SEARCH_URL, func, ic, HTTP_GET, args, 2); } /** * Show debug information for an account. */ void mastodon_search_account(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_log_all); } /** * Show debug information for the relationship with an account. */ void mastodon_relationship(struct im_connection *ic, guint64 id) { char *args[2] = { "id", g_strdup_printf("%" G_GUINT64_FORMAT, id), }; mastodon_http(ic, MASTODON_ACCOUNT_RELATIONSHIP_URL, mastodon_http_log_all, ic, HTTP_GET, args, 2); g_free(args[1]); } /** * Callback to print debug information about a relationship. */ static void mastodon_http_search_relationship(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_account *ma = mastodon_xt_get_user(parsed); if (!ma) { mastodon_log(ic, "Couldn't find a matching account."); goto finish; } char *args[2] = { "id", g_strdup_printf("%" G_GUINT64_FORMAT, ma->id), }; mastodon_http(ic, MASTODON_ACCOUNT_RELATIONSHIP_URL, mastodon_http_log_all, ic, HTTP_GET, args, 2); g_free(args[1]); finish: ma_free(ma); json_value_free(parsed); } /** * Search for an account and and show debug information for the * relationship with the first account found. */ void mastodon_search_relationship(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_search_relationship); } /** * Show debug information for a status. */ void mastodon_status(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_STATUS_URL, id); mastodon_http(ic, url, mastodon_http_log_all, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Allow the user to make a raw request. */ void mastodon_raw(struct im_connection *ic, char *method, char *url, char **arguments, int arguments_len) { http_method_t m = HTTP_GET; if (g_ascii_strcasecmp(method, "get") == 0) { m = HTTP_GET; } else if (g_ascii_strcasecmp(method, "put") == 0) { m = HTTP_PUT; } else if (g_ascii_strcasecmp(method, "post") == 0) { m = HTTP_POST; } else if (g_ascii_strcasecmp(method, "delete") == 0) { m = HTTP_DELETE; } mastodon_http(ic, url, mastodon_http_log_all, ic, m, arguments, arguments_len); } /** * Callback for showing the URL of a status. */ static void mastodon_http_status_show_url(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic); if (ms) { mastodon_log(ic, ms->url); ms_free(ms); } else { mastodon_log(ic, "Error: could not fetch toot url."); } json_value_free(parsed); } /** * Show the URL for a status. */ void mastodon_status_show_url(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_STATUS_URL, id); mastodon_http(ic, url, mastodon_http_status_show_url, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Append a the acct attribute to a gstring user_data, separated with a space, if necessary. This is to be used with * g_list_foreach(). The prefix "@" is added in front of every element. */ static void mastodon_account_append(struct mastodon_account *ma, GString *user_data) { if (user_data->len > 0) { g_string_append(user_data, " "); } g_string_append(user_data, "@"); g_string_append(user_data, ma->acct); } /** * Join all the acct attributes of a list of accounts, space-separated. Be sure to free the returned GString with * g_string_free(). If there is no initial element for the list, use NULL for the second argument. The prefix "@" is * added in front of every element. It is added to the initial element, too! This is used to generated a list of * accounts to mention in a toot. */ GString *mastodon_account_join(GSList *l, gchar *init) { if (!l && !init) return NULL; GString *s = g_string_new(NULL); if (init) { g_string_append(s, "@"); g_string_append(s, init); } g_slist_foreach(l, (GFunc) mastodon_account_append, s); return s; } /** * Show the list of mentions for a status in our log data. */ void mastodon_show_mentions(struct im_connection *ic, GSList *l) { if (l) { GString *s = mastodon_account_join(l, NULL); mastodon_log(ic, "Mentioned: %s", s->str); g_string_free(s, TRUE); } else { mastodon_log(ic, "Nobody was mentioned in this toot"); } } /** * Callback for showing the mentions of a status. */ static void mastodon_http_status_show_mentions(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic); if (ms) { mastodon_show_mentions(ic, ms->mentions); ms_free(ms); } else { mastodon_log(ic, "Error: could not fetch toot url."); } json_value_free(parsed); } /** * Show the mentions for a status. */ void mastodon_status_show_mentions(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_STATUS_URL, id); mastodon_http(ic, url, mastodon_http_status_show_mentions, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Attempt to flush the context data. This is called by the two * callbacks for the context request because we need to wait for two * responses: the original status details, and the context itself. */ void mastodon_flush_context(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; if (!(md->flags & MASTODON_GOT_STATUS) || !(md->flags & MASTODON_GOT_CONTEXT)) { return; } struct mastodon_status *ms = md->status_obj; struct mastodon_list *bl = md->context_before_obj; struct mastodon_list *al = md->context_after_obj; GSList *l; for (l = bl->list; l; l = g_slist_next(l)) { struct mastodon_status *s = (struct mastodon_status *) l->data; mastodon_status_show_chat(ic, s); } mastodon_status_show_chat(ic, ms); for (l = al->list; l; l = g_slist_next(l)) { struct mastodon_status *s = (struct mastodon_status *) l->data; mastodon_status_show_chat(ic, s); } ml_free(al); ml_free(bl); ms_free(ms); md->flags &= ~(MASTODON_GOT_STATUS | MASTODON_GOT_CONTEXT); md->status_obj = md->context_before_obj = md->context_after_obj = NULL; } /** * Callback for the context of a status. Store it in our mastodon data * structure and attempt to flush it. */ void mastodon_http_context(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } struct mastodon_data *md = ic->proto_data; json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto end; } if (parsed->type != json_object) { goto finished; } struct mastodon_list *bl = g_new0(struct mastodon_list, 1); struct mastodon_list *al = g_new0(struct mastodon_list, 1); json_value *before = json_o_get(parsed, "ancestors"); json_value *after = json_o_get(parsed, "descendants"); if (before->type == json_array && mastodon_xt_get_status_list(ic, before, bl)) { md->context_before_obj = bl; } if (after->type == json_array && mastodon_xt_get_status_list(ic, after, al)) { md->context_after_obj = al; } finished: json_value_free(parsed); end: if (ic) { md->flags |= MASTODON_GOT_CONTEXT; mastodon_flush_context(ic); } } /** * Callback for the original status as part of a context request. * Store it in our mastodon data structure and attempt to flush it. */ void mastodon_http_context_status(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } struct mastodon_data *md = ic->proto_data; json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto end; } md->status_obj = mastodon_xt_get_status(parsed, ic); json_value_free(parsed); end: if (ic) { md->flags |= MASTODON_GOT_STATUS; mastodon_flush_context(ic); } } /** * Search for a status and its context. The problem is that the * context doesn't include the status we're interested in. That's why * we must make two requests and wait until we get the response to * both. */ void mastodon_context(struct im_connection *ic, guint64 id) { struct mastodon_data *md = ic->proto_data; ms_free(md->status_obj); ml_free(md->context_before_obj); ml_free(md->context_after_obj); md->status_obj = md->context_before_obj = md->context_after_obj = NULL; md->flags &= ~(MASTODON_GOT_STATUS | MASTODON_GOT_CONTEXT); char *url = g_strdup_printf(MASTODON_STATUS_CONTEXT_URL, id); mastodon_http(ic, url, mastodon_http_context, ic, HTTP_GET, NULL, 0); g_free(url); url = g_strdup_printf(MASTODON_STATUS_URL, id); mastodon_http(ic, url, mastodon_http_context_status, ic, HTTP_GET, NULL, 0); g_free(url); } /** * The callback functions for mastodon_http_statuses_chain() should look like this, e.g. mastodon_account_statuses or * mastodon_account_pinned_statuses. The way this works: If you know the id of an account, call the function directly: * mastodon_account_statuses(). The callback for this is mastodon_http_statuses() which uses mastodon_http_timeline() to * display the statuses. If you don't know the id of an account by you know the "who" of the account, call a function * like mastodon_unknown_account_statuses(). It calls mastodon_with_search_account() and the callback for this is * mastodon_http_unknown_account_statuses(). The http_request it gets has the account(s) you need. Call * mastodon_chained_account() which parses the request and determines the actual account id. Finally, it calls the last * callback, which mastodon_account_statuses(), with the account id. And we already know how that works! Phew!! * */ typedef void (*mastodon_chained_account_function)(struct im_connection *ic, guint64 id); void mastodon_chained_account(struct http_request *req, mastodon_chained_account_function func) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "Couldn't find a matching account."); goto finish; } // Just use the first one, let's hope these are sorted appropriately! struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[0]); if (ma) { func(ic, ma->id); } else { mastodon_log(ic, "Couldn't find a matching account."); } ma_free(ma); finish: json_value_free(parsed); } /** * Callback for a reponse containing one or more statuses which are to be shown, usually the result of looking at the * statuses of an account. */ void mastodon_http_statuses(struct http_request *req) { mastodon_http_timeline(req, MT_HOME); } /** * Given a command that showed a bunch of statuses, which will have used mastodon_http_timeline as a callback, and which * will therefore have set md->next_url, we can use now use this URL to request more statuses. */ void mastodon_more(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; if (!md->next_url) { mastodon_log(ic, "Next URL is not set. This shouldn't happen, as they say!?"); return; } char *url = g_strdup(md->next_url); char *s = NULL; int len = 0; int i; for (i = 0; url[i]; i++) { if (url[i] == '?') { url[i] = 0; s = url + i + 1; len = 1; } else if (s && url[i] == '&') { url[i] = '='; // for later splitting len++; } } gchar **args = NULL; if (s) { args = g_strsplit (s, "=", -1); } switch(md->more_type) { case MASTODON_MORE_STATUSES: mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, args, len); break; case MASTODON_MORE_NOTIFICATIONS: mastodon_http(ic, url, mastodon_http_notifications, ic, HTTP_GET, args, len); break; } g_strfreev(args); g_free(url); } /** * Show the timeline of a user. */ void mastodon_account_statuses(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_ACCOUNT_STATUSES_URL, id); mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Show the pinned statuses of a user. */ void mastodon_account_pinned_statuses(struct im_connection *ic, guint64 id) { char *args[2] = { "pinned", "1", }; char *url = g_strdup_printf(MASTODON_ACCOUNT_STATUSES_URL, id); mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, args, 2); g_free(url); } /** * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user * and display their timeline. */ void mastodon_http_unknown_account_statuses(struct http_request *req) { mastodon_chained_account(req, mastodon_account_statuses); } /** * Show the timeline of an unknown user. Thus, we first have to search for them. */ void mastodon_unknown_account_statuses(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_unknown_account_statuses); } /** * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user * and display their timeline. */ void mastodon_http_unknown_account_pinned_statuses(struct http_request *req) { mastodon_chained_account(req, mastodon_account_pinned_statuses); } /** * Show the timeline of an unknown user. Thus, we first have to search for them. */ void mastodon_unknown_account_pinned_statuses(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_unknown_account_pinned_statuses); } /** * Callback for the user bio. */ void mastodon_http_account_bio(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } const char *display_name = json_o_str(parsed, "display_name"); char *note = g_strdup(json_o_str(parsed, "note")); mastodon_strip_html(note); // modified in place mastodon_log(ic, "Bio for %s: %s", display_name, note); g_free(note); json_value_free(parsed); } /** * Show a user bio. */ void mastodon_account_bio(struct im_connection *ic, guint64 id) { char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id); mastodon_http(ic, url, mastodon_http_account_bio, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user * and show their bio. */ void mastodon_http_unknown_account_bio(struct http_request *req) { mastodon_chained_account(req, mastodon_account_bio); } /** * Show the bio of an unknown user. Thus, we first have to search for them. */ void mastodon_unknown_account_bio(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_unknown_account_bio); } /** * Call back for step 3 of mastodon_follow: adding the buddy. */ static void mastodon_http_follow3(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_account *ma = mastodon_xt_get_user(parsed); if (ma) { mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name); mastodon_log(ic, "You are now following %s.", ma->acct); } else { mastodon_log(ic, "Couldn't find a matching account."); } ma_free(ma); json_value_free(parsed); } /** * Call back for step 2 of mastodon_follow: actually following. */ static void mastodon_http_follow2(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } json_value *it; if ((it = json_o_get(parsed, "domain_blocking")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "This user's domain is being blocked by your instance."); } if ((it = json_o_get(parsed, "blocking")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "You need to unblock this user."); } if ((it = json_o_get(parsed, "muting")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "You might want to unmute this user."); } if ((it = json_o_get(parsed, "muting")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "You might want to unmute this user."); } if ((it = json_o_get(parsed, "requested")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "You have requested to follow this user."); } if ((it = json_o_get(parsed, "followed_by")) && it->type == json_boolean && it->u.boolean) { mastodon_log(ic, "Nice, this user is already following you."); } if ((it = json_o_get(parsed, "following")) && it->type == json_boolean && it->u.boolean) { guint64 id; if ((it = json_o_get(parsed, "id")) && (id = mastodon_json_int64(it))) { char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id); mastodon_http(ic, url, mastodon_http_follow3, ic, HTTP_GET, NULL, 0); g_free(url); } else { mastodon_log(ic, "I can't believe it: this relation has no id. I can't add them!"); } } json_value_free(parsed); } /** * Call back for step 1 of mastodon_follow: searching for the account to follow. */ static void mastodon_http_follow1(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "Couldn't find a matching account."); goto finish; } // Just use the first one, let's hope these are sorted appropriately! struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[0]); if (ma) { char *url = g_strdup_printf(MASTODON_ACCOUNT_FOLLOW_URL, ma->id); mastodon_http(ic, url, mastodon_http_follow2, ic, HTTP_POST, NULL, 0); g_free(url); ma_free(ma); } else { mastodon_log(ic, "Couldn't find a matching account."); } finish: json_value_free(parsed); } /** * Function to follow an unknown user. First we need to search for it, * though. */ void mastodon_follow(struct im_connection *ic, char *who) { mastodon_with_search_account(ic, who, mastodon_http_follow1); } /** * Callback for adding the buddies you are following. */ static void mastodon_http_following(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { goto finish; } int i; for (i = 0; i < parsed->u.array.length; i++) { struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]); if (ma) { mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name); } ma_free(ma); } finish: json_value_free(parsed); gboolean done = TRUE; // try to fetch more if there is a header saying that there is // more (URL in angled brackets) char *header = NULL; if ((header = get_rfc822_header(req->reply_headers, "Link", 0))) { char *url = NULL; char *s = NULL; int len = 0; int i; for (i = 0; header[i]; i++) { if (header[i] == '<') { url = header + i + 1; } else if (header[i] == '?') { header[i] = 0; // end url s = header + i + 1; len = 1; } else if (s && header[i] == '&') { header[i] = '='; // for later splitting len++; } else if (url && header[i] == '>') { header[i] = 0; if (strncmp(header + i + 1, "; rel=\"next\"", 12) == 0) { break; } else { url = NULL; s = NULL; len = 0; } } } if (url) { gchar **args = NULL; if (s) { args = g_strsplit (s, "=", -1); } mastodon_http(ic, url, mastodon_http_following, ic, HTTP_GET, args, len); done = FALSE; g_strfreev(args); } g_free(header); } if (done) { /* Now that we have reached the end of the list, everybody has mastodon_user_data set, at last: imcb_add_buddy → bee_user_new → ic->acc->prpl->buddy_data_add → mastodon_buddy_data_add. Now we're ready to (re)load lists. */ mastodon_list_reload(ic, TRUE); struct mastodon_data *md = ic->proto_data; md->flags |= MASTODON_HAVE_FRIENDS; } } /** * Add the buddies the current account is following. */ void mastodon_following(struct im_connection *ic) { gint64 id = set_getint(&ic->acc->set, "account_id"); if (!id) { return; } char *url = g_strdup_printf(MASTODON_ACCOUNT_FOLLOWING_URL, id); mastodon_http(ic, url, mastodon_http_following, ic, HTTP_GET, NULL, 0); g_free(url); } /** * Callback for the list of lists. */ void mastodon_http_lists(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "Use 'list create <name>' to create a list."); goto finish; } int i; GString *s = g_string_new(g_strdup_printf("Lists: ")); gboolean first = TRUE; for (i = 0; i < parsed->u.array.length; i++) { json_value *a = parsed->u.array.values[i]; if (a->type == json_object) { if (first) { first = FALSE; } else { g_string_append(s, "; "); } g_string_append(s, json_o_str(a, "title")); } } mastodon_log(ic, s->str); g_string_free(s, TRUE); finish: json_value_free(parsed); } /** * Retrieving lists. Returns at most 50 Lists without pagination. */ void mastodon_lists(struct im_connection *ic) { mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_lists, ic, HTTP_GET, NULL, 0); } /** * Create a list. */ void mastodon_list_create(struct im_connection *ic, char *title) { struct mastodon_data *md = ic->proto_data; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; if (md->undo_type == MASTODON_NEW) { mc->command = MC_LIST_CREATE; mc->redo = g_strdup_printf("list create %s", title); mc->undo = g_strdup_printf("list delete %s", title); } char *args[2] = { "title", title, }; mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 2); } /** * Second callback for the list of accounts. */ void mastodon_http_list_accounts2(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { goto finally; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finally; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "There are no members in this list. Your options:\n" "Delete it using 'list delete %s'\n" "Add members using 'list add <nick> to %s'", mc->str, mc->str); goto finish; } int i; GString *m = g_string_new("Members:"); for (i = 0; i < parsed->u.array.length; i++) { struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]); if (ma) { g_string_append(m, " "); bee_user_t *bu = bee_user_by_handle(ic->bee, ic, ma->acct); if (bu) { irc_user_t *iu = bu->ui_data; g_string_append(m, iu->nick); } else { g_string_append(m, "@"); g_string_append(m, ma->acct); } ma_free(ma); } } mastodon_log(ic, m->str); g_string_free(m, TRUE); finish: /* We need to free the parsed data. */ json_value_free(parsed); finally: /* We've encountered a problem and we need to free the mastodon_command. */ mc_free(mc); } /** * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination. */ void mastodon_list_accounts(struct im_connection *ic, struct mastodon_command *mc) { char *args[2] = { "limit", "0", }; char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id); mastodon_http(ic, url, mastodon_http_list_accounts2, mc, HTTP_GET, args, 2); g_free(url); } /** * First callback for listing the accounts in a list. First, get the list id from the data we received, then call the * next function. */ void mastodon_http_list_accounts(struct http_request *req) { mastodon_chained_list(req, mastodon_list_accounts); } /** * Show accounts in a list. */ void mastodon_unknown_list_accounts(struct im_connection *ic, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->str = g_strdup(title); mastodon_with_named_list(ic, mc, mastodon_http_list_accounts); } /** * Second callback for list delete. We get back the accounts in the list we are about to delete. We need these to * prepare the undo command. Undo is serious business. Then we delete the list. */ void mastodon_http_list_delete2(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; struct mastodon_data *md = ic->proto_data; if (!g_slist_find(mastodon_connections, ic)) { goto finish; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finish; } if (parsed->type != json_array || parsed->u.array.length == 0) { mastodon_log(ic, "There are no members in this list. Cool!"); } else if (md->undo_type == MASTODON_NEW) { int i; char *title = mc->str; GString *undo = g_string_new(mc->undo); for (i = 0; i < parsed->u.array.length; i++) { struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]); if (ma) { g_string_append(undo, FS); g_string_append_printf(undo, "list add %" G_GINT64_FORMAT " to %s", ma->id, title); } ma_free(ma); } g_free(mc->undo); mc->undo = undo->str; // adopt g_string_free(undo, FALSE); } char *url = g_strdup_printf(MASTODON_LIST_DATA_URL, mc->id); mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, NULL, 0); g_free(url); goto success; finish: mc_free(mc); success: json_value_free(parsed); } /** * This is part of the first callback. We have the list id in mc->id! If this is a new command, we want to find all the * accounts in the list in order to prepare the undo command. Undo is serious business. If this command is not new, in * other words, this is a redo command, then we can skip right ahead and go for the list delete. */ void mastodon_list_delete(struct im_connection *ic, struct mastodon_command *mc) { struct mastodon_data *md = ic->proto_data; if (md->undo_type == MASTODON_NEW) { /* Make sure we get all the accounts for undo. The API documentation says: If you specify limit=0 in the query, * all accounts will be returned without pagination. */ char *args[2] = { "limit", "0", }; char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id); mastodon_http(ic, url, mastodon_http_list_delete2, mc, HTTP_GET, args, 2); g_free(url); } else { /* This is a short cut! */ char *url = g_strdup_printf(MASTODON_LIST_DATA_URL, mc->id); mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, NULL, 0); g_free(url); } } /** * Callback for list delete. We get back the lists we know about and need to find the list to delete. Once we have it, * we use another callback to get the list of all its members in order to prepare the undo command. Undo is serious * business. */ void mastodon_http_list_delete(struct http_request *req) { mastodon_chained_list(req, mastodon_list_delete); } /** * Delete a list by title. */ void mastodon_unknown_list_delete(struct im_connection *ic, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->str = g_strdup(title); struct mastodon_data *md = ic->proto_data; if (md->undo_type == MASTODON_NEW) { mc->command = MC_LIST_DELETE; mc->redo = g_strdup_printf("list delete %s", title); mc->undo = g_strdup_printf("list create %s", title); } mastodon_with_named_list(ic, mc, mastodon_http_list_delete); }; /** * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination. */ void mastodon_list_add_account(struct im_connection *ic, struct mastodon_command *mc) { char *args[2] = { "account_ids[]", g_strdup_printf("%" G_GUINT64_FORMAT, mc->id2), }; char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id); mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 2); g_free(args[1]); g_free(url); } /** * First callback for adding an account to a list. First, get the list id from the data we received, then call the next * function. */ void mastodon_http_list_add_account(struct http_request *req) { mastodon_chained_list(req, mastodon_list_add_account); } /** * Add one or more accounts to a list. */ void mastodon_unknown_list_add_account(struct im_connection *ic, guint64 id, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->id2 = id; mc->str = g_strdup(title); struct mastodon_data *md = ic->proto_data; if (md->undo_type == MASTODON_NEW) { mc->command = MC_LIST_ADD_ACCOUNT; mc->redo = g_strdup_printf("list add %" G_GINT64_FORMAT " to %s", id, title); mc->undo = g_strdup_printf("list remove %" G_GINT64_FORMAT " from %s", id, title); } mastodon_with_named_list(ic, mc, mastodon_http_list_add_account); }; /** * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination. */ void mastodon_list_remove_account(struct im_connection *ic, struct mastodon_command *mc) { char *args[2] = { "account_ids[]", g_strdup_printf("%" G_GUINT64_FORMAT, mc->id2), }; char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id); mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, args, 2); g_free(args[1]); g_free(url); } /** * First callback for removing an accounts from a list. First, get the list id from the data we received, then call the * next function. */ void mastodon_http_list_remove_account(struct http_request *req) { mastodon_chained_list(req, mastodon_list_remove_account); } /** * Remove one or more accounts from a list. */ void mastodon_unknown_list_remove_account(struct im_connection *ic, guint64 id, char *title) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->id2 = id; mc->str = g_strdup(title); struct mastodon_data *md = ic->proto_data; if (md->undo_type == MASTODON_NEW) { mc->command = MC_LIST_REMOVE_ACCOUNT; mc->redo = g_strdup_printf("list remove %" G_GINT64_FORMAT " from %s", id, title); mc->undo = g_strdup_printf("list add %" G_GINT64_FORMAT " to %s", id, title); } mastodon_with_named_list(ic, mc, mastodon_http_list_remove_account); } /** * Second callback to reload all the lists. We are getting all the accounts for one of the lists, here. * The mastodon_command (mc) has id (id of the list), str (title of the list), and optionally extra. */ static void mastodon_http_list_reload2(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { goto finally; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finally; } if (parsed->type != json_array || parsed->u.array.length == 0) { goto finish; } int i; for (i = 0; i < parsed->u.array.length; i++) { struct mastodon_account *ma; bee_user_t *bu; struct mastodon_user_data *mud; if ((ma = mastodon_xt_get_user(parsed->u.array.values[i])) && (bu = bee_user_by_handle(ic->bee, ic, ma->acct)) && (mud = (struct mastodon_user_data*) bu->data)) { mud->lists = g_slist_prepend(mud->lists, g_strdup(mc->str)); ma_free(ma); } } mastodon_log(ic, "Membership of %s list reloaded", mc->str); finish: json_value_free(parsed); if (mc->extra) { /* Keep using the mc and don't free it! */ mastodon_list_timeline(ic, mc); return; } finally: /* We've encountered a problem and we need to free the mastodon_command. */ mc_free(mc); } /** * First callback to reload all the lists. We are getting all the lists, here. For each one, get the members in the * list. Our mastodon_command (mc) might have the extra attribute set. */ static void mastodon_http_list_reload(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { goto finally; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; goto finally; } if (parsed->type != json_array || parsed->u.array.length == 0) { goto finish; } /* Clear existing list membership. */ GSList *l; for (l = ic->bee->users; l; l = l->next) { bee_user_t *bu = l->data; struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data; if (mud) { g_slist_free_full(mud->lists, g_free); mud->lists = NULL; } } int i; guint64 id = 0; /* Get members for every list defined. */ for (i = 0; i < parsed->u.array.length; i++) { json_value *a = parsed->u.array.values[i]; json_value *it; const char *title; if (a->type == json_object && (it = json_o_get(a, "id")) && (id = mastodon_json_int64(it)) && (title = json_o_str(a, "title"))) { struct mastodon_command *mc2 = g_new0(struct mastodon_command, 1); mc2->ic = ic; mc2->id = id; mc2->str = g_strdup(title); mc2->extra = mc->extra; char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, id); mastodon_http(ic, url, mastodon_http_list_reload2, mc2, HTTP_GET, NULL, 0); g_free(url); } } finish: json_value_free(parsed); finally: /* We've encountered a problem and we need to free the mastodon_command. */ mc_free(mc); } /** * Reload the memberships of all the lists. We need this for mastodon_status_show_chat(). The populate parameter says * whether we should issue a timeline request for every list we have a group chat for, at the end. */ void mastodon_list_reload(struct im_connection *ic, gboolean populate) { struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->extra = populate; mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_list_reload, mc, HTTP_GET, NULL, 0); } /** * Free the md->filter list so that it can be done from mastodon_logout(). */ void mastodon_filters_destroy(struct mastodon_data *md) { GSList *l; for (l = md->filters; l; l = g_slist_next(l)) { mf_free((struct mastodon_filter *) l->data); } g_slist_free(md->filters); md->filters = NULL; } /** * Parse the context attribute of a Mastodon filter. */ mastodon_filter_type_t mastodon_parse_context(json_value *parsed) { mastodon_filter_type_t context = 0; int i; for (i = 0; i < parsed->u.array.length; i++) { json_value *s = parsed->u.array.values[i]; if (s->type == json_string) { if (g_ascii_strcasecmp(s->u.string.ptr, "home") == 0) context |= MF_HOME; if (g_ascii_strcasecmp(s->u.string.ptr, "notifications") == 0) context |= MF_NOTIFICATIONS; if (g_ascii_strcasecmp(s->u.string.ptr, "public") == 0) context |= MF_PUBLIC; if (g_ascii_strcasecmp(s->u.string.ptr, "thread") == 0) context |= MF_THREAD; } } return context; } /** * Parse a filter. */ struct mastodon_filter *mastodon_parse_filter (json_value *parsed) { json_value *it; guint64 id = 0; const char *phrase; if (parsed && parsed->type == json_object && (it = json_o_get(parsed, "id")) && (id = mastodon_json_int64(it)) && (phrase = json_o_str(parsed, "phrase"))) { struct mastodon_filter *mf = g_new0(struct mastodon_filter, 1); mf->id = id; mf->phrase = g_strdup(phrase); mf->phrase_case_folded = g_utf8_casefold(phrase, -1); if ((it = json_o_get(parsed, "context")) && it->type == json_array) mf->context = mastodon_parse_context(it); if ((it = json_o_get(parsed, "irreversible")) && it->type == json_boolean) mf->irreversible = it->u.boolean; if ((it = json_o_get(parsed, "whole_word")) && it->type == json_boolean) mf->whole_word = it->u.boolean; struct tm time; if ((it = json_o_get(parsed, "expires_in")) && it->type == json_string && strptime(it->u.string.ptr, MASTODON_TIME_FORMAT, &time) != NULL) mf->expires_in = mktime_utc(&time); return mf; } return NULL; } /** * Callback for loading filters. We need to do this when connecting to the instance, and we want to do it when * displaying the filters. */ void mastodon_http_filters_load (struct http_request *req) { struct im_connection *ic = req->data; struct mastodon_data *md = ic->proto_data; if (!g_slist_find(mastodon_connections, ic)) { return; } if (req->status_code != 200) { mastodon_log(ic, "Filters did not load. This requires Mastodon v2.4.3 or newer. See 'info instance' for more about your instance."); // Don't log off: no ic = NULL! return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } if (parsed->type != json_array || parsed->u.array.length == 0) { goto finish; } mastodon_filters_destroy(md); int i; for (i = 0; i < parsed->u.array.length; i++) { json_value *it = parsed->u.array.values[i]; struct mastodon_filter *mf = mastodon_parse_filter(it); if (mf) md->filters = g_slist_prepend(md->filters, mf); } finish: json_value_free(parsed); } /** * Callback for reloading and displaying filters. */ void mastodon_http_filters (struct http_request *req) { struct im_connection *ic = req->data; struct mastodon_data *md = ic->proto_data; mastodon_http_filters_load(req); if (!md->filters) { mastodon_log(ic, "No filters. Use 'filter create'."); return; } GSList *l; int i = 1; for (l = md->filters; l; l = g_slist_next(l)) { struct mastodon_filter *mf = (struct mastodon_filter *) l->data; GString *p = g_string_new(NULL); int mask = MF_HOME|MF_PUBLIC|MF_NOTIFICATIONS|MF_THREAD; if ((mf->context & mask) == mask) { g_string_append(p, " everywhere"); } else { if (mf->context & MF_HOME) { g_string_append(p, " home"); } if (mf->context & MF_PUBLIC) { g_string_append(p, " public"); } if (mf->context & MF_NOTIFICATIONS) { g_string_append(p, " notifications"); } if (mf->context & MF_THREAD) { g_string_append(p, " thread"); } } if (mf->irreversible) { g_string_append(p, ", server side"); } if (mf->whole_word) { g_string_append(p, ", whole word"); } mastodon_log(ic, "%2d. %s (properties:%s)", i++, mf->phrase, p->str); g_string_free(p, TRUE); } } /** * Load and display the filters from the instance. */ void mastodon_filters(struct im_connection *ic) { mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_filters, ic, HTTP_GET, NULL, 0); } /** * Callback for mastodon_get_filters. */ void mastodon_http_get_filters (struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } mastodon_http_filters_load(req); struct mastodon_data *md = ic->proto_data; md->flags |= MASTODON_GOT_FILTERS; mastodon_flush_timeline(ic); } /** * See mastodon_initial_timeline. */ static void mastodon_get_filters(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; md->flags &= ~MASTODON_GOT_FILTERS; mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_get_filters, ic, HTTP_GET, NULL, 0); } /** * Callback for filter creation. We need to get the number of the filter created and use that for the undo command. */ void mastodon_http_filter_create(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if (!(parsed = mastodon_parse_response(ic, req))) { /* ic would have been freed in imc_logout in this situation */ ic = NULL; return; } struct mastodon_filter *mf = mastodon_parse_filter(parsed); if (mf) { struct mastodon_data *md = ic->proto_data; md->filters = g_slist_prepend(md->filters, mf); mastodon_log(ic, "Filter created"); /* Maintain undo/redo list. */ mc->undo = g_strdup_printf("filter delete %" G_GUINT64_FORMAT, mf->id); if(md->undo_type == MASTODON_NEW) { mastodon_do(ic, mc->redo, mc->undo); } else { mastodon_do_update(ic, mc->undo); } } } /** * Create a new filter. */ void mastodon_filter_create(struct im_connection *ic, char *str) { struct mastodon_data *md = ic->proto_data; struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; if (md->undo_type == MASTODON_NEW) { mc->command = MC_FILTER_CREATE; mc->redo = g_strdup_printf("filter create %s", str); } /* FIXME: add timeout */ char *args[14] = { "phrase", str, "context[]", "home", "context[]", "notifications", "context[]", "public", "context[]", "thread", "irreversible", "true", "whole_words", "1", }; mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_filter_create, mc, HTTP_POST, args, 14); } /** * Callback for filter deletion. */ void mastodon_http_filter_delete(struct http_request *req) { struct mastodon_command *mc = req->data; struct im_connection *ic = mc->ic; if (!g_slist_find(mastodon_connections, ic)) { return; } if (req->status_code == 200) { struct mastodon_data *md = ic->proto_data; struct mastodon_filter *mf = (struct mastodon_filter *) mc->data; md->filters = g_slist_remove(md->filters, mf); mastodon_http_callback_and_ack(req); } } /** * Delete a filter based on the item number when listing them. */ void mastodon_filter_delete(struct im_connection *ic, char *arg) { guint64 id; if (!parse_int64(arg, 10, &id)) { mastodon_log(ic, "You must refer to a filter number. Use 'filter' to list them."); return; } struct mastodon_data *md = ic->proto_data; /* filters are listed starting at 1 */ struct mastodon_filter *mf = (struct mastodon_filter *) g_slist_nth_data(md->filters, id - 1); if (!mf) { GSList *l; gboolean found = FALSE; for (l = md->filters; l; l = g_slist_next(l)) { mf = (struct mastodon_filter *) l->data; if (mf->id == id) { found = TRUE; break; } } if (!found) { mastodon_log(ic, "This filter is unkown. Use 'filter' to list them."); return; } } struct mastodon_command *mc = g_new0(struct mastodon_command, 1); mc->ic = ic; mc->data = (gpointer *) mf; if (md->undo_type == MASTODON_NEW) { mc->command = MC_FILTER_DELETE; /* FIXME: more parameters */ mc->redo = g_strdup_printf("filter delete %" G_GUINT64_FORMAT, mf->id); mc->undo = g_strdup_printf("filter create %s", mf->phrase); } char *url = g_strdup_printf(MASTODON_FILTER_DATA_URL, mf->id); mastodon_http(ic, url, mastodon_http_filter_delete, mc, HTTP_DELETE, NULL, 0); g_free(url); } /** * Callback for getting your own account. This saves the account_id. * Once we have that, we are ready to figure out who our followers are. */ static void mastodon_http_verify_credentials(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } json_value *parsed; if ((parsed = mastodon_parse_response(ic, req))) { json_value *it; guint64 id; if ((it = json_o_get(parsed, "id")) && (id = mastodon_json_int64(it))) { set_setint(&ic->acc->set, "account_id", id); } json_value_free(parsed); mastodon_following(ic); } } /** * Get the account of the current user. */ void mastodon_verify_credentials(struct im_connection *ic) { imcb_log(ic, "Verifying credentials"); mastodon_http(ic, MASTODON_VERIFY_CREDENTIALS_URL, mastodon_http_verify_credentials, ic, HTTP_GET, NULL, 0); } /** * Callback for registering a new application. */ static void mastodon_http_register_app(struct http_request *req) { struct im_connection *ic = req->data; if (!g_slist_find(mastodon_connections, ic)) { return; } mastodon_log(ic, "Parsing application registration response"); json_value *parsed; if ((parsed = mastodon_parse_response(ic, req))) { set_setint(&ic->acc->set, "app_id", json_o_get(parsed, "id")->u.integer); char *key = json_o_strdup(parsed, "client_id"); char *secret = json_o_strdup(parsed, "client_secret"); json_value_free(parsed); // save for future sessions set_setstr(&ic->acc->set, "consumer_key", key); set_setstr(&ic->acc->set, "consumer_secret", secret); // and set for the current session, and connect struct mastodon_data *md = ic->proto_data; struct oauth2_service *os = md->oauth2_service; os->consumer_key = key; os->consumer_secret = secret; oauth2_init(ic); } } /** * Function to register a new application (Bitlbee) for the server. */ void mastodon_register_app(struct im_connection *ic) { char *args[8] = { "client_name", "bitlbee", "redirect_uris", "urn:ietf:wg:oauth:2.0:oob", "scopes", "read write follow", "website", "https://www.bitlbee.org/" }; mastodon_http(ic, MASTODON_REGISTER_APP_URL, mastodon_http_register_app, ic, HTTP_POST, args, 8); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon-lib.h�����������������������������������������������������������0000664�0000000�0000000�00000020703�13432067562�0020307�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2017-2018 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ #pragma once #include "nogaim.h" #include "mastodon-http.h" #define MASTODON_API_URL "https://octodon.social/api/v1" // "2017-08-02T10:45:03.000Z" -- but we're ignoring microseconds and UTC timezone #define MASTODON_TIME_FORMAT "%Y-%m-%dT%H:%M:%S" #define MASTODON_REGISTER_APP_URL "/apps" #define MASTODON_VERIFY_CREDENTIALS_URL "/accounts/verify_credentials" #define MASTODON_STREAMING_USER_URL "/streaming/user" #define MASTODON_STREAMING_HASHTAG_URL "/streaming/hashtag" #define MASTODON_STREAMING_LOCAL_URL "/streaming/public/local" #define MASTODON_STREAMING_FEDERATED_URL "/streaming/public" #define MASTODON_STREAMING_LIST_URL "/streaming/list" #define MASTODON_HOME_TIMELINE_URL "/timelines/home" #define MASTODON_PUBLIC_TIMELINE_URL "/timelines/public" #define MASTODON_HASHTAG_TIMELINE_URL "/timelines/tag/%s" #define MASTODON_LIST_TIMELINE_URL "/timelines/list/%" G_GINT64_FORMAT #define MASTODON_NOTIFICATIONS_URL "/notifications" #define MASTODON_REPORT_URL "/reports" #define MASTODON_SEARCH_URL "/search" #define MASTODON_INSTANCE_URL "/instance" #define MASTODON_STATUS_POST_URL "/statuses" #define MASTODON_STATUS_URL "/statuses/%" G_GINT64_FORMAT #define MASTODON_STATUS_BOOST_URL "/statuses/%" G_GINT64_FORMAT "/reblog" #define MASTODON_STATUS_UNBOOST_URL "/statuses/%" G_GINT64_FORMAT "/unreblog" #define MASTODON_STATUS_MUTE_URL "/statuses/%" G_GINT64_FORMAT "/mute" #define MASTODON_STATUS_UNMUTE_URL "/statuses/%" G_GINT64_FORMAT "/unmute" #define MASTODON_STATUS_FAVOURITE_URL "/statuses/%" G_GINT64_FORMAT "/favourite" #define MASTODON_STATUS_UNFAVOURITE_URL "/statuses/%" G_GINT64_FORMAT "/unfavourite" #define MASTODON_STATUS_PIN_URL "/statuses/%" G_GINT64_FORMAT "/pin" #define MASTODON_STATUS_UNPIN_URL "/statuses/%" G_GINT64_FORMAT "/unpin" #define MASTODON_STATUS_CONTEXT_URL "/statuses/%" G_GINT64_FORMAT "/context" #define MASTODON_ACCOUNT_URL "/accounts/%" G_GINT64_FORMAT #define MASTODON_ACCOUNT_SEARCH_URL "/accounts/search" #define MASTODON_ACCOUNT_STATUSES_URL "/accounts/%" G_GINT64_FORMAT "/statuses" #define MASTODON_ACCOUNT_FOLLOWING_URL "/accounts/%" G_GINT64_FORMAT "/following" #define MASTODON_ACCOUNT_BLOCK_URL "/accounts/%" G_GINT64_FORMAT "/block" #define MASTODON_ACCOUNT_UNBLOCK_URL "/accounts/%" G_GINT64_FORMAT "/unblock" #define MASTODON_ACCOUNT_FOLLOW_URL "/accounts/%" G_GINT64_FORMAT "/follow" #define MASTODON_ACCOUNT_UNFOLLOW_URL "/accounts/%" G_GINT64_FORMAT "/unfollow" #define MASTODON_ACCOUNT_MUTE_URL "/accounts/%" G_GINT64_FORMAT "/mute" #define MASTODON_ACCOUNT_UNMUTE_URL "/accounts/%" G_GINT64_FORMAT "/unmute" #define MASTODON_ACCOUNT_LISTS "/accounts/%" G_GINT64_FORMAT "/lists" #define MASTODON_LIST_URL "/lists" #define MASTODON_LIST_DATA_URL "/lists/%" G_GINT64_FORMAT #define MASTODON_LIST_ACCOUNTS_URL "/lists/%" G_GINT64_FORMAT "/accounts" #define MASTODON_FILTER_URL "/filters" #define MASTODON_FILTER_DATA_URL "/filters/%" G_GINT64_FORMAT #define MASTODON_ACCOUNT_RELATIONSHIP_URL "/accounts/relationships" typedef enum { MASTODON_EVT_UNKNOWN, MASTODON_EVT_UPDATE, MASTODON_EVT_NOTIFICATION, MASTODON_EVT_DELETE, } mastodon_evt_flags_t; void mastodon_register_app(struct im_connection *ic); void mastodon_verify_credentials(struct im_connection *ic); void mastodon_notifications(struct im_connection *ic); void mastodon_initial_timeline(struct im_connection *ic); void mastodon_hashtag_timeline(struct im_connection *ic, char *hashtag); void mastodon_home_timeline(struct im_connection *ic); void mastodon_local_timeline(struct im_connection *ic); void mastodon_federated_timeline(struct im_connection *ic); void mastodon_open_user_stream(struct im_connection *ic); void mastodon_unknown_list_timeline(struct im_connection *ic, char *title); struct http_request *mastodon_open_hashtag_stream(struct im_connection *ic, char *hashtag); struct http_request *mastodon_open_local_stream(struct im_connection *ic); struct http_request *mastodon_open_federated_stream(struct im_connection *ic); void mastodon_open_unknown_list_stream(struct im_connection *ic, struct groupchat *c, char *title); mastodon_visibility_t mastodon_default_visibility(struct im_connection *ic); mastodon_visibility_t mastodon_parse_visibility(char *value); char *mastodon_visibility(mastodon_visibility_t visibility); void mastodon_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to, mastodon_visibility_t visibility, char *spoiler_text); void mastodon_post(struct im_connection *ic, char *format, mastodon_command_type_t command, guint64 id); GString *mastodon_account_join(GSList *l, gchar *init); void mastodon_show_mentions(struct im_connection *ic, GSList *l); void mastodon_status_show_mentions(struct im_connection *ic, guint64 id); void mastodon_status_show_url(struct im_connection *ic, guint64 id); void mastodon_report(struct im_connection *ic, guint64 id, char *comment); void mastodon_follow(struct im_connection *ic, char *who); void mastodon_status_delete(struct im_connection *ic, guint64 id); void mastodon_instance(struct im_connection *ic); void mastodon_account(struct im_connection *ic, guint64 id); void mastodon_search_account(struct im_connection *ic, char *who); void mastodon_status(struct im_connection *ic, guint64 id); void mastodon_raw(struct im_connection *ic, char *method, char *url, char **arguments, int arguments_len); void mastodon_relationship(struct im_connection *ic, guint64 id); void mastodon_search_relationship(struct im_connection *ic, char *who); void mastodon_search(struct im_connection *ic, char *what); void mastodon_context(struct im_connection *ic, guint64 id); void mastodon_more(struct im_connection *ic); void mastodon_account_statuses(struct im_connection *ic, guint64 id); void mastodon_unknown_account_statuses(struct im_connection *ic, char *who); void mastodon_account_pinned_statuses(struct im_connection *ic, guint64 id); void mastodon_unknown_account_pinned_statuses(struct im_connection *ic, char *who); void mastodon_account_bio(struct im_connection *ic, guint64 id); void mastodon_unknown_account_bio(struct im_connection *ic, char *who); void mastodon_lists(struct im_connection *ic); void mastodon_list_create(struct im_connection *ic, char *title); void mastodon_unknown_list_accounts(struct im_connection *ic, char *title); void mastodon_unknown_list_delete(struct im_connection *ic, char *title); void mastodon_unknown_list_add_account(struct im_connection *ic, guint64 id, char *title); void mastodon_unknown_list_remove_account(struct im_connection *ic, guint64 id, char *title); void mastodon_list_reload(struct im_connection *ic, gboolean populate); void mastodon_filters_destroy(struct mastodon_data *md); void mastodon_filters(struct im_connection *ic); void mastodon_filter_create(struct im_connection *ic, char *str); void mastodon_filter_delete(struct im_connection *ic, char *arg); �������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon.c���������������������������������������������������������������0000664�0000000�0000000�00000154174�13432067562�0017550�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net> * * Copyright 2017-2018 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ #include "nogaim.h" #include "oauth.h" #include "oauth2.h" #include "mastodon.h" #include "mastodon-http.h" #include "mastodon-lib.h" #include "rot13.h" #include "url.h" #include "help.h" #define HELPFILE_NAME "mastodon-help.txt" static void mastodon_help_init() { /* Figure out where our help file is by looking at the global helpfile. */ gchar *dir = g_path_get_dirname (global.helpfile); if (strcmp(dir, ".") == 0) { log_message(LOGLVL_WARNING, "Error finding the directory of helpfile %s.", global.helpfile); g_free(dir); return; } gchar *df = g_strjoin("/", dir, HELPFILE_NAME, NULL); g_free(dir); /* Load help from our own help file. */ help_t *dh; help_init(&dh, df); if(dh == NULL) { log_message(LOGLVL_WARNING, "Error opening helpfile: %s.", df); g_free(df); return; } g_free(df); /* Link the last entry of global.help with first entry of our help. */ help_t *h, *l = NULL; for (h = global.help; h; h = h->next) { l = h; } if (l) { l->next = dh; } else { /* No global help but ours? */ global.help = dh; } } #ifdef BITLBEE_ABI_VERSION_CODE struct plugin_info *init_plugin_info(void) { /* Run ./configure to change these. */ static struct plugin_info info = { BITLBEE_ABI_VERSION_CODE, PACKAGE_NAME, PACKAGE_VERSION, "Bitlbee plugin for Mastodon <https://joinmastodon.org/>", "Alex Schroeder <alex@gnu.org>", "https://alexschroeder.ch/cgit/bitlbee-mastodon/about/" }; return &info; } #endif GSList *mastodon_connections = NULL; struct groupchat *mastodon_groupchat_init(struct im_connection *ic) { struct groupchat *gc; struct mastodon_data *md = ic->proto_data; GSList *l; if (md->timeline_gc) { return md->timeline_gc; } md->timeline_gc = gc = imcb_chat_new(ic, "mastodon/timeline"); imcb_chat_name_hint(gc, md->name); for (l = ic->bee->users; l; l = l->next) { bee_user_t *bu = l->data; if (bu->ic == ic) { imcb_chat_add_buddy(gc, bu->handle); } } imcb_chat_add_buddy(gc, ic->acc->user); return gc; } /** * Free the oauth2_service struct. */ static void os_free(struct oauth2_service *os) { if (os == NULL) { return; } g_free(os->auth_url); g_free(os->token_url); g_free(os); } /** * Create a new oauth2_service struct. If we haven never connected to * the server, we'll be missing our key and secret. */ static struct oauth2_service *get_oauth2_service(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; struct oauth2_service *os = g_new0(struct oauth2_service, 1); os->auth_url = g_strconcat("https://", md->url_host, "/oauth/authorize", NULL); os->token_url = g_strconcat("https://", md->url_host, "/oauth/token", NULL); os->redirect_url = "urn:ietf:wg:oauth:2.0:oob"; os->scope = MASTODON_SCOPE; // possibly empty strings if the client is not registered os->consumer_key = set_getstr(&ic->acc->set, "consumer_key"); os->consumer_secret = set_getstr(&ic->acc->set, "consumer_secret"); return os; } /** * Check message length by comparing it to the appropriate setting. * Note this issue: "Count all URLs in text as 23 characters flat, do * not count domain part of usernames." * https://github.com/tootsuite/mastodon/pull/4427 **/ static gboolean mastodon_length_check(struct im_connection *ic, gchar *msg, char *cw) { int len = g_utf8_strlen(msg, -1); if (len == 0) { mastodon_log(ic, "This message is empty."); return FALSE; } if (cw != NULL) { len += g_utf8_strlen(cw, -1); } int max = set_getint(&ic->acc->set, "message_length"); if (max == 0) { return TRUE; } GRegex *regex = g_regex_new (MASTODON_URL_REGEX, 0, 0, NULL); GMatchInfo *match_info; g_regex_match (regex, msg, 0, &match_info); while (g_match_info_matches (match_info)) { gchar *url = g_match_info_fetch (match_info, 0); len = len - g_utf8_strlen(url, -1) + 23; g_free (url); g_match_info_next (match_info, NULL); } g_regex_unref (regex); regex = g_regex_new (MASTODON_MENTION_REGEX, 0, 0, NULL); g_regex_match (regex, msg, 0, &match_info); while (g_match_info_matches (match_info)) { gchar *mention = g_match_info_fetch (match_info, 0); gchar *nick = g_match_info_fetch (match_info, 2); len = len - g_utf8_strlen(mention, -1) + g_utf8_strlen(nick, -1); g_free (mention); g_free (nick); g_match_info_next (match_info, NULL); } g_regex_unref (regex); g_match_info_free (match_info); if (len <= max) { return TRUE; } mastodon_log(ic, "Maximum message length exceeded: %d > %d", len, max); return FALSE; } static char *set_eval_commands(set_t * set, char *value) { if (g_ascii_strcasecmp(value, "strict") == 0) { return value; } else { return set_eval_bool(set, value); } } static char *set_eval_mode(set_t * set, char *value) { if (g_ascii_strcasecmp(value, "one") == 0 || g_ascii_strcasecmp(value, "many") == 0 || g_ascii_strcasecmp(value, "chat") == 0) { return value; } else { return NULL; } } static char *set_eval_hide_sensitive(set_t * set, char *value) { if (g_ascii_strcasecmp(value, "rot13") == 0 || g_ascii_strcasecmp(value, "advanced_rot13") == 0) { return value; } else { return set_eval_bool(set, value); } } static char *set_eval_visibility(set_t * set, char *value) { if (g_ascii_strcasecmp(value, "public") == 0 || g_ascii_strcasecmp(value, "unlisted") == 0 || g_ascii_strcasecmp(value, "private") == 0) { return value; } else { return "public"; } } static void mastodon_init(account_t * acc) { set_t *s; char *def_url; if (strcmp(acc->prpl->name, "mastodon") == 0) { def_url = MASTODON_API_URL; } s = set_add(&acc->set, "auto_reply_timeout", "10800", set_eval_int, acc); s = set_add(&acc->set, "base_url", def_url, NULL, acc); s->flags |= ACC_SET_OFFLINE_ONLY; s = set_add(&acc->set, "commands", "true", set_eval_commands, acc); s = set_add(&acc->set, "message_length", "500", set_eval_int, acc); s = set_add(&acc->set, "mode", "chat", set_eval_mode, acc); s->flags |= ACC_SET_OFFLINE_ONLY; s = set_add(&acc->set, "name", "", NULL, acc); s->flags |= ACC_SET_OFFLINE_ONLY; s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc); s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc); s = set_add(&acc->set, "hide_sensitive", "false", set_eval_hide_sensitive, acc); s = set_add(&acc->set, "sensitive_flag", "*NSFW* ", NULL, acc); s = set_add(&acc->set, "visibility", "public", set_eval_visibility, acc); s = set_add(&acc->set, "hide_boosts", "false", set_eval_bool, acc); s = set_add(&acc->set, "hide_favourites", "false", set_eval_bool, acc); s = set_add(&acc->set, "hide_mentions", "false", set_eval_bool, acc); s = set_add(&acc->set, "hide_follows", "false", set_eval_bool, acc); s = set_add(&acc->set, "app_id", "0", set_eval_int, acc); s->flags |= SET_HIDDEN; s = set_add(&acc->set, "account_id", "0", set_eval_int, acc); s->flags |= SET_HIDDEN; s = set_add(&acc->set, "consumer_key", "", NULL, acc); s->flags |= SET_HIDDEN; s = set_add(&acc->set, "consumer_secret", "", NULL, acc); s->flags |= SET_HIDDEN; mastodon_help_init(); } /** * Set the name of the Mastodon channel, either based on a preference, or based on hostname and account name. */ static void mastodon_set_name(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; char *name = set_getstr(&ic->acc->set, "name"); if (name[0]) { md->name = g_strdup(name); } else { md->name = g_strdup_printf("%s_%s", md->url_host, ic->acc->user); } } /** * Connect to Mastodon server, using the data we saved in the account. */ static void mastodon_connect(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; url_t url; char *s; imcb_log(ic, "Connecting"); if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) || url.proto != PROTO_HTTPS) { imcb_error(ic, "Incorrect API base URL: %s", set_getstr(&ic->acc->set, "base_url")); imc_logout(ic, FALSE); return; } md->url_ssl = url.proto == PROTO_HTTPS; // always md->url_port = url.port; md->url_host = g_strdup(url.host); if (strcmp(url.file, "/") != 0) { md->url_path = g_strdup(url.file); } mastodon_set_name(ic); imcb_add_buddy(ic, md->name, NULL); imcb_buddy_status(ic, md->name, OPT_LOGGED_IN, NULL, NULL); md->log = g_new0(struct mastodon_log_data, MASTODON_LOG_LENGTH); md->log_id = -1; s = set_getstr(&ic->acc->set, "mode"); if (g_ascii_strcasecmp(s, "one") == 0) { md->flags |= MASTODON_MODE_ONE; } else if (g_ascii_strcasecmp(s, "many") == 0) { md->flags |= MASTODON_MODE_MANY; } else { md->flags |= MASTODON_MODE_CHAT; } if (!(md->flags & MASTODON_MODE_ONE) && !(md->flags & MASTODON_HAVE_FRIENDS)) { // find our account_id and store it, eventually mastodon_verify_credentials(ic); } /* Create the room. */ if (md->flags & MASTODON_MODE_CHAT) { mastodon_groupchat_init(ic); } mastodon_initial_timeline(ic); mastodon_open_user_stream(ic); ic->flags |= OPT_PONGS; } /** * Initiate OAuth dialog with user. A reply to the MASTODON_OAUTH_HANDLE is handled by mastodon_buddy_msg. */ void oauth2_init(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; imcb_log(ic, "Starting OAuth authentication"); /* Temporary contact, just used to receive the OAuth response. */ imcb_add_buddy(ic, MASTODON_OAUTH_HANDLE, NULL); char *url = oauth2_url(md->oauth2_service); char *msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url); imcb_buddy_msg(ic, MASTODON_OAUTH_HANDLE, msg, 0, 0); g_free(msg); g_free(url); imcb_buddy_msg(ic, MASTODON_OAUTH_HANDLE, "Respond to this message with the returned " "authorization token.", 0, 0); ic->flags |= OPT_SLOW_LOGIN; } int oauth2_refresh(struct im_connection *ic, const char *refresh_token); static void mastodon_login(account_t * acc) { struct im_connection *ic = imcb_new(acc); struct mastodon_data *md = g_new0(struct mastodon_data, 1); url_t url; imcb_log(ic, "Login"); mastodon_connections = g_slist_append(mastodon_connections, ic); ic->proto_data = md; md->user = g_strdup(acc->user); if (!url_set(&url, set_getstr(&ic->acc->set, "base_url"))) { imcb_error(ic, "Cannot parse API base URL: %s", set_getstr(&ic->acc->set, "base_url")); imc_logout(ic, FALSE); return; } if (url.proto != PROTO_HTTPS) { imcb_error(ic, "API base URL must use HTTPS: %s", set_getstr(&ic->acc->set, "base_url")); imc_logout(ic, FALSE); return; } if (strcmp(url.file, "/api/v1") != 0) { imcb_log(ic, "API base URL should probably end in /api/v1 instead of %s", url.file); } md->url_ssl = 1; md->url_port = url.port; md->url_host = g_strdup(url.host); if (strcmp(url.file, "/") != 0) { md->url_path = g_strdup(url.file); } else { md->url_path = g_strdup(""); } mastodon_set_name(ic); GSList *p_in = NULL; const char *tok; md->oauth2_service = get_oauth2_service(ic); oauth_params_parse(&p_in, ic->acc->pass); /* If we did not have these stored, register the app and try * again. We'll call oauth2_init from the callback in order to * connect, eventually. */ if (!md->oauth2_service->consumer_key || !md->oauth2_service->consumer_secret || strlen(md->oauth2_service->consumer_key) == 0 || strlen(md->oauth2_service->consumer_secret) == 0) { mastodon_register_app(ic); } /* If we have a refresh token, in which case any access token we *might* have has probably expired already anyway. Refresh and connect. */ else if ((tok = oauth_params_get(&p_in, "refresh_token"))) { oauth2_refresh(ic, tok); } /* If we don't have a refresh token, let's hope the access token is still usable. */ else if ((tok = oauth_params_get(&p_in, "access_token"))) { md->oauth2_access_token = g_strdup(tok); mastodon_connect(ic); } /* If we don't have any, start the OAuth process now. */ else { oauth2_init(ic); } /* All of the above will end up calling mastodon_connect(). */ oauth_params_free(&p_in); } /** * Logout method. Just free the mastodon_data. */ static void mastodon_logout(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; // Set the status to logged out. ic->flags &= ~OPT_LOGGED_IN; if (md) { if (md->timeline_gc) { imcb_chat_free(md->timeline_gc); } GSList *l; for (l = md->streams; l; l = l->next) { struct http_request *req = l->data; http_close(req); } g_slist_free(md->streams); md->streams = NULL; if (md->log) { /* When mastodon_connect hasn't been called, yet, such as when imc_logout is being called from * mastodon_login, the log hasn not yet been initialised. */ int i; for (i = 0; i < MASTODON_LOG_LENGTH; i++) { g_slist_free_full(md->log[i].mentions, g_free); md->log[i].mentions = NULL; g_free(md->log[i].spoiler_text); } g_free(md->log); md->log = NULL; } mastodon_filters_destroy(md); g_slist_free_full(md->mentions, g_free); md->mentions = NULL; g_free(md->last_spoiler_text); md->last_spoiler_text = NULL; g_free(md->spoiler_text); md->spoiler_text = NULL; os_free(md->oauth2_service); md->oauth2_service = NULL; g_free(md->user); md->user = NULL; g_free(md->name); md->name = NULL; g_free(md->next_url); md->next_url = NULL; g_free(md->url_host); md->url_host = NULL; g_free(md->url_path); md->url_path = NULL; g_free(md); ic->proto_data = NULL; } mastodon_connections = g_slist_remove(mastodon_connections, ic); } /** * When the user replies to the MASTODON_OAUTH_HANDLE with a refresh token we request the access token and this is where * we get it. Save both in our settings and proceed to mastodon_connect. */ void oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error) { struct im_connection *ic = data; struct mastodon_data *md; GSList *auth = NULL; if (g_slist_find(mastodon_connections, ic) == NULL) { return; } md = ic->proto_data; if (access_token == NULL) { imcb_error(ic, "OAuth failure (%s)", error); imc_logout(ic, TRUE); return; } oauth_params_parse(&auth, ic->acc->pass); if (refresh_token) { oauth_params_set(&auth, "refresh_token", refresh_token); } if (access_token) { oauth_params_set(&auth, "access_token", access_token); } g_free(ic->acc->pass); ic->acc->pass = oauth_params_string(auth); oauth_params_free(&auth); g_free(md->oauth2_access_token); md->oauth2_access_token = g_strdup(access_token); mastodon_connect(ic); } static gboolean oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond) { struct im_connection *ic = data; if (g_slist_find(mastodon_connections, ic)) { imcb_remove_buddy(ic, MASTODON_OAUTH_HANDLE, NULL); } return FALSE; } /** * Get the refresh token from the user via a reply to MASTODON_OAUTH_HANDLE in mastodon_buddy_msg. * Then get the access token Using the refresh token. The access token is then handled by oauth2_got_token. */ int oauth2_get_refresh_token(struct im_connection *ic, const char *msg) { struct mastodon_data *md = ic->proto_data; char *code; int ret; imcb_log(ic, "Requesting OAuth access token"); /* Don't do it here because the caller may get confused if the contact we're currently sending a message to is deleted. */ b_timeout_add(1, oauth2_remove_contact, ic); code = g_strdup(msg); g_strstrip(code); ret = oauth2_access_token(md->oauth2_service, OAUTH2_AUTH_CODE, code, oauth2_got_token, ic); g_free(code); return ret; } int oauth2_refresh(struct im_connection *ic, const char *refresh_token) { struct mastodon_data *md = ic->proto_data; return oauth2_access_token(md->oauth2_service, OAUTH2_AUTH_REFRESH, refresh_token, oauth2_got_token, ic); } /** * Post a message. Make sure we get all the meta data for the status right. */ static void mastodon_post_message(struct im_connection *ic, char *message, guint64 in_reply_to, char *who, mastodon_message_t type, GSList *mentions, mastodon_visibility_t visibility, char *spoiler_text) { struct mastodon_data *md = ic->proto_data; char *text = NULL; GString *m = NULL; int wlen; char *s; switch (type) { case MASTODON_DIRECT: visibility = MV_DIRECT; // fall through case MASTODON_REPLY: /* Mentioning OP and other mentions is the traditional thing to do. Note that who can be NULL if we're redoing a command like "redo 1234567 foo" where we didn't get any user info from the status id. */ if (!who) break; if (g_ascii_strcasecmp(who, md->user) == 0) { /* if replying to ourselves, we still want to mention others, if any */ m = mastodon_account_join(mentions, NULL); } else { /* if replying to others, mention them, too */ m = mastodon_account_join(mentions, who); } if (m) { text = g_strdup_printf("%s %s", m->str, message); g_string_free(m, TRUE); } /* Note that visibility and spoiler_text have already been set, no need to do anything else. */ break; case MASTODON_NEW_MESSAGE: visibility = md->visibility; /* Note that at the end we will use the default visibility if this is NULL. */ break; case MASTODON_MAYBE_REPLY: { g_assert(visibility == MV_UNKNOWN); wlen = strlen(who); // length of the first word // If the message starts with "nick:" or "nick," if (who && wlen && strncmp(who, message, wlen) == 0 && (s = message + wlen - 1) && (*s == ':' || *s == ',')) { // Trim punctuation from who. who[wlen - 1] = '\0'; // Determine what we are replying to. bee_user_t *bu; if ((bu = bee_user_by_handle(ic->bee, ic, who))) { struct mastodon_user_data *mud = bu->data; if (time(NULL) < mud->last_time + set_getint(&ic->acc->set, "auto_reply_timeout")) { // this is a reply in_reply_to = mud->last_id; // We're always replying to at least one person. bu->handle is fully qualified unlike who m = mastodon_account_join(mud->mentions, bu->handle); visibility = mud->visibility; spoiler_text = mud->spoiler_text; } else { // this is a new message but we still need to prefix the @ and use bu->handle instead of who m = g_string_new("@"); g_string_append(m, bu->handle); } // use +wlen+1 to remove "nick: " (note the space) from message text = g_strdup_printf("%s %s", m->str, message + wlen + 1); g_string_free(m, TRUE); } else if (g_ascii_strcasecmp(who, md->user) == 0) { /* Compare case-insensitively because this is user input. */ /* Same as a above but replying to myself and therefore using mastodon_data (md). We don't set this data to NULL because we might want to send multiple replies to ourselves. We want this to work on a slow instance, so the user can send multiple replies without having to wait for replies to come back and set these values again via mastodon_http_callback. */ in_reply_to = md->last_id; visibility = md->last_visibility; spoiler_text = g_strdup(md->last_spoiler_text); if (md->mentions) { m = mastodon_account_join(md->mentions, NULL); mastodon_log(ic, "Mentions %s", m->str); text = g_strdup_printf("%s %s", m->str, message + wlen + 1); g_string_free(m, TRUE); } else { // use +wlen+1 to remove "nick: " (note the space) from message message += wlen + 1; } } } } break; } if (!mastodon_length_check(ic, text ? text : message, md->spoiler_text ? md->spoiler_text : spoiler_text)) { goto finish; } /* If we explicitly set a visibility for the next toot, use that. Otherwise, use the visibility as determined above, * but make sure that a higher default visibility takes precedence: higher means more private. See * mastodon_visibility_t. */ if (md->visibility != MV_UNKNOWN) { visibility = md->visibility; } else { mastodon_visibility_t default_visibility = mastodon_default_visibility(ic); if (default_visibility > visibility) visibility = default_visibility; } /* md->spoiler_text set by the CW command and md->visibility set by the VISIBILITY command take precedence and get * removed after posting. */ mastodon_post_status(ic, text ? text : message, in_reply_to, visibility, md->spoiler_text ? md->spoiler_text : spoiler_text); g_free(md->spoiler_text); md->spoiler_text = NULL; md->visibility = MV_UNKNOWN; finish: g_free(text); g_free(spoiler_text); } static void mastodon_handle_command(struct im_connection *ic, char *message, mastodon_undo_t undo_type); /** * Send a direct message. If this buddy is the magic mastodon oauth * handle, then treat the message as the refresh token. If this buddy * is me, then treat the message as a command. */ static int mastodon_buddy_msg(struct im_connection *ic, char *who, char *message, int away) { struct mastodon_data *md = ic->proto_data; if (g_ascii_strcasecmp(who, MASTODON_OAUTH_HANDLE) == 0 && !(md->flags & OPT_LOGGED_IN)) { if (oauth2_get_refresh_token(ic, message)) { return 1; } else { imcb_error(ic, "OAuth failure"); imc_logout(ic, TRUE); return 0; } } if (g_ascii_strcasecmp(who, md->name) == 0) { mastodon_handle_command(ic, message, MASTODON_NEW); } else { mastodon_post_message(ic, message, 0, who, MASTODON_REPLY, NULL, MV_DIRECT, NULL); } return 0; } static void mastodon_user(struct im_connection *ic, char *who); static void mastodon_get_info(struct im_connection *ic, char *who) { struct mastodon_data *md = ic->proto_data; struct irc_channel *ch = md->timeline_gc->ui_data; imcb_log(ic, "Sending output to %s", ch->name); if (g_ascii_strcasecmp(who, md->name) == 0) { mastodon_instance(ic); } else { mastodon_user(ic, who); } } static void mastodon_chat_msg(struct groupchat *c, char *message, int flags) { if (c && message) { mastodon_handle_command(c->ic, message, MASTODON_NEW); } } /** * Joining a group chat means showing the appropriate timeline and start streaming it. */ static struct groupchat *mastodon_chat_join(struct im_connection *ic, const char *room, const char *nick, const char *password, set_t **sets) { char *topic = g_strdup(room); struct groupchat *c = imcb_chat_new(ic, topic); imcb_chat_topic(c, NULL, topic, 0); imcb_chat_add_buddy(c, ic->acc->user); struct http_request *req = NULL; if (strcmp(topic, "local") == 0) { mastodon_local_timeline(ic); req = mastodon_open_local_stream(ic); } else if (strcmp(topic, "federated") == 0) { mastodon_federated_timeline(ic); req = mastodon_open_federated_stream(ic); } else if (topic[0] == '#') { mastodon_hashtag_timeline(ic, topic + 1); req = mastodon_open_hashtag_stream(ic, topic + 1); } else { /* After the initial login we cannot be sure that an initial list timeline will work because the lists are not loaded, yet. That's why mastodon_following() will end up reloading the lists with the extra parameter which will load these timelines. If we're creating this channel at a later point, however, this should be possible. One way to determine if we're "at a later point" is by looking at MASTODON_HAVE_FRIENDS. It's actually not quite correct: at this point we have the lists but not the list members, but it should be good enough as we're only interested in later chat joining, not auto_join. */ struct mastodon_data *md = ic->proto_data; if (md->flags & MASTODON_HAVE_FRIENDS) { mastodon_unknown_list_timeline(ic, topic); } /* We need to identify the list we're going to stream but we don't get a request on the return from mastodon_open_unknown_list_stream(). Instead, we pass the channel along and when we have the list, the request will be set accordingly. */ mastodon_open_unknown_list_stream(ic, c, topic); } g_free(topic); c->data = req; return c; } /** * If the user leaves the main channel: Fine. Rejoin him/her once new toots come in. But what if the user leaves a * channel that is connected to a stream? In this case we need to find the appropriate stream and close it, too. */ static void mastodon_chat_leave(struct groupchat *c) { GSList *l; struct mastodon_data *md = c->ic->proto_data; if (c == md->timeline_gc) { md->timeline_gc = NULL; } else { struct http_request *stream = c->data; for (l = md->streams; l; l = l->next) { struct http_request *req = l->data; if (stream == req) { md->streams = g_slist_remove(md->streams, req); http_close(req); break; } } } imcb_chat_free(c); } static void mastodon_add_permit(struct im_connection *ic, char *who) { } static void mastodon_rem_permit(struct im_connection *ic, char *who) { } static void mastodon_buddy_data_add(bee_user_t *bu) { bu->data = g_new0(struct mastodon_user_data, 1); } static void mastodon_buddy_data_free(bee_user_t *bu) { struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data; g_slist_free_full(mud->lists, g_free); mud->lists = NULL; g_slist_free_full(mud->mentions, g_free); mud->mentions = NULL; g_free(mud->spoiler_text); mud->spoiler_text = NULL; g_free(bu->data); } bee_user_t mastodon_log_local_user; /** * Find a user account based on their nick name. */ static bee_user_t *mastodon_user_by_nick(struct im_connection *ic, char *nick) { GSList *l; for (l = ic->bee->users; l; l = l->next) { bee_user_t *bu = l->data; irc_user_t *iu = bu->ui_data; if (g_ascii_strcasecmp(iu->nick, nick) == 0) { /* Compare case-insentively because this is user input. */ return bu; } } return NULL; } /** * Convert the given bitlbee toot ID or bitlbee username into a * mastodon status ID and returns it. If provided with a pointer to a * bee_user_t, fills that as well. Provide NULL if you don't need it. * The same is true for mentions, visibility and spoiler text. * * Returns 0 if the user provides garbage. */ static guint64 mastodon_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_, GSList **mentions_, mastodon_visibility_t *visibility_, char **spoiler_text_) { struct mastodon_data *md = ic->proto_data; struct mastodon_user_data *mud; bee_user_t *bu = NULL; guint64 id = 0; if (bu_) { *bu_ = NULL; } if (!arg || !arg[0]) { return 0; } if (arg[0] != '#' && (bu = mastodon_user_by_nick(ic, arg))) { if ((mud = bu->data)) { id = mud->last_id; if (mentions_) *mentions_ = mud->mentions; if (visibility_) *visibility_ = mud->visibility; if (spoiler_text_) *spoiler_text_ = mud->spoiler_text; } } else { if (arg[0] == '#') { arg++; } if (parse_int64(arg, 16, &id) && id < MASTODON_LOG_LENGTH) { if (mentions_) *mentions_ = md->log[id].mentions; if (visibility_) *visibility_ = md->log[id].visibility; if (spoiler_text_) *spoiler_text_ = md->log[id].spoiler_text; bu = md->log[id].bu; id = md->log[id].id; } else if (parse_int64(arg, 10, &id)) { /* Allow normal toot IDs as well. Required do undo posts, for example. */ } else { /* Reset id if id was a valid hex number but >= MASTODON_LOG_LENGTH. */ id = 0; } } if (bu_) { if (bu == &mastodon_log_local_user) { /* HACK alert. There's no bee_user object for the local * user so just fake one for the few cmds that need it. */ mastodon_log_local_user.handle = md->user; } else { /* Beware of dangling pointers! */ if (!g_slist_find(ic->bee->users, bu)) { bu = NULL; } } *bu_ = bu; } return id; } static void mastodon_no_id_warning(struct im_connection *ic, char *what) { mastodon_log(ic, "User or status '%s' is unknown.", what); } static void mastodon_unknown_user_warning(struct im_connection *ic, char *who) { mastodon_log(ic, "User '%s' is unknown.", who); } /** * Get the message id given a nick or a status id. If possible, also set a number of other variables by reference. */ static guint64 mastodon_message_id_or_warn_and_more(struct im_connection *ic, char *what, bee_user_t **bu, GSList **mentions, mastodon_visibility_t *visibility, char **spoiler_text) { guint64 id = mastodon_message_id_from_command_arg(ic, what, bu, mentions, visibility, spoiler_text); if (!id) { mastodon_no_id_warning(ic, what); } return id; } /** * Simple interface to mastodon_message_id_or_warn_and_more. Get the message id given a nick or a status id. */ static guint64 mastodon_message_id_or_warn(struct im_connection *ic, char *what) { return mastodon_message_id_or_warn_and_more(ic, what, NULL, NULL, NULL, NULL); } static guint64 mastodon_account_id(bee_user_t *bu) { struct mastodon_user_data *mud; if (bu != NULL && (mud = bu->data)) { return mud->account_id; } return 0; } static guint64 mastodon_user_id_or_warn(struct im_connection *ic, char *who) { bee_user_t *bu; guint64 id; if ((bu = mastodon_user_by_nick(ic, who)) && (id = mastodon_account_id(bu))) { return id; } else if (parse_int64(who, 10, &id)) { return id; } mastodon_unknown_user_warning(ic, who); return 0; } static void mastodon_user(struct im_connection *ic, char *who) { bee_user_t *bu; guint64 id; if ((bu = mastodon_user_by_nick(ic, who)) && (id = mastodon_account_id(bu))) { mastodon_account(ic, id); } else { mastodon_search_account(ic, who); } } static void mastodon_relation_to_user(struct im_connection *ic, char *who) { bee_user_t *bu; guint64 id; if ((bu = mastodon_user_by_nick(ic, who)) && (id = mastodon_account_id(bu))) { mastodon_relationship(ic, id); } else { mastodon_search_relationship(ic, who); } } static void mastodon_add_buddy(struct im_connection *ic, char *who, char *group) { bee_user_t *bu; guint64 id; if ((bu = mastodon_user_by_nick(ic, who)) && (id = mastodon_account_id(bu))) { // If the nick is already in the channel (when we just // unfollowed them, for example), we're taking a // shortcut. No fancy looking at the relationship and // all that. The nick is already here, after all. mastodon_post(ic, MASTODON_ACCOUNT_FOLLOW_URL, MC_FOLLOW, id); } else if (parse_int64(who, 10, &id)) { // If we provided a numerical id, then that will also // work. This is used by redo/undo. mastodon_post(ic, MASTODON_ACCOUNT_FOLLOW_URL, MC_FOLLOW, id); } else { // Alternatively, we're looking for an unknown user. // They must be searched, followed, and added to the // channel. It's going to take more requests. mastodon_follow(ic, who); } } static void mastodon_remove_buddy(struct im_connection *ic, char *who, char *group) { guint64 id; if ((id = mastodon_user_id_or_warn(ic, who))) { mastodon_post(ic, MASTODON_ACCOUNT_UNFOLLOW_URL, MC_UNFOLLOW, id); } } static void mastodon_add_deny(struct im_connection *ic, char *who) { guint64 id; if ((id = mastodon_user_id_or_warn(ic, who))) { mastodon_post(ic, MASTODON_ACCOUNT_BLOCK_URL, MC_BLOCK, id); } } static void mastodon_rem_deny(struct im_connection *ic, char *who) { guint64 id; if ((id = mastodon_user_id_or_warn(ic, who))) { mastodon_post(ic, MASTODON_ACCOUNT_UNBLOCK_URL, MC_UNBLOCK, id); } } /** * Add a command and a way to undo it to the undo stack. Remember that * only the callback knows whether a command succeeded or not, and * what the id of a newly posted status is, and all that. Thus, * there's a delay that we need to take into account. * * The stack is organized as follows if we just did D: * 0 1 2 3 4 5 6 7 8 9 * undo = [a b c d e f g h i j] * redo = [A B C D E F G H I J] * first_undo = 3 * current_undo = 3 * If we do X: * undo = [a b c d x f g h i j] * redo = [A B C D X F G H I J] * first_undo = 4 * current_undo = 4 * If we undo it, send x and: * undo = [a b c d x f g h i j] * redo = [A B C D X F G H I J] * first_undo = 4 * current_undo = 3 * If we redo, send X and increase current_undo. * If we undo instead, send d and decrease current_undo again: * undo = [a b c d x f g h i j] * redo = [A B C D X F G H I J] * first_undo = 4 * current_undo = 2 * If we do Y with current_undo different from first_undo, null the tail: * undo = [a b c y 0 f g h i j] * redo = [A B C Y 0 F G H I J] * first_undo = 3 * current_undo = 3 */ void mastodon_do(struct im_connection *ic, char *redo, char *undo) { struct mastodon_data *md = ic->proto_data; int i = (md->current_undo + 1) % MASTODON_MAX_UNDO; g_free(md->redo[i]); g_free(md->undo[i]); md->redo[i] = redo; md->undo[i] = undo; if (md->current_undo == md->first_undo) { md->current_undo = md->first_undo = i; } else { md->current_undo = i; int end = (md->first_undo + 1) % MASTODON_MAX_UNDO; for (i = (md->current_undo + 1) % MASTODON_MAX_UNDO; i != end; i = (i + 1) % MASTODON_MAX_UNDO) { g_free(md->redo[i]); g_free(md->undo[i]); md->redo[i] = NULL; md->undo[i] = NULL; } md->first_undo = md->current_undo; } } /** * Undo the last command. */ void mastodon_undo(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; char *cmd = md->undo[md->current_undo]; if (!cmd) { mastodon_log(ic, "There is nothing to undo."); return; } gchar **cmds = g_strsplit (cmd, FS, -1); int i; for (i = 0; cmds[i]; i++) { mastodon_handle_command(ic, cmds[i], MASTODON_UNDO); } g_strfreev(cmds); // beware of negatives and modulo md->current_undo = (md->current_undo + MASTODON_MAX_UNDO - 1) % MASTODON_MAX_UNDO; } /** * Redo the last command. Multiple commands can be executed as one using the ASCII Field Separator (FS). */ void mastodon_redo(struct im_connection *ic) { struct mastodon_data *md = ic->proto_data; if (md->current_undo == md->first_undo) { mastodon_log(ic, "There is nothing to redo."); return; } md->current_undo = (md->current_undo + 1) % MASTODON_MAX_UNDO; char *cmd = md->redo[md->current_undo]; gchar **cmds = g_strsplit (cmd, FS, -1); int i; for (i = 0; cmds[i]; i++) { mastodon_handle_command(ic, cmds[i], MASTODON_REDO); } g_strfreev(cmds); } /** * Update the current command in the stack. This is necessary when * executing commands which change references that we saved. For * example: every delete statement refers to an id. Whenever a post * happens because of redo, the delete command in the undo stack has * to be replaced. Whenever a post happens because of undo, the delete * command in the redo stack has to be replaced. * * We make our own copies of 'to'. */ void mastodon_do_update(struct im_connection *ic, char *to) { struct mastodon_data *md = ic->proto_data; char *from = NULL; int i; switch (md->undo_type) { case MASTODON_NEW: // should not happen return; case MASTODON_UNDO: // after post due to undo of a delete statement, the // old delete statement is in the next redo element i = (md->current_undo + 1) % MASTODON_MAX_UNDO; from = g_strdup(md->redo[i]); break; case MASTODON_REDO: // after post due to redo of a post statement, the // old delete statement is in the undo element i = md->current_undo; from = g_strdup(md->undo[i]); break; } /* After a post and a delete of that post, there are at least * two cells where the old reference can be hiding (undo of * the post and redo of the delete). Brute force! */ for (i = 0; i < MASTODON_MAX_UNDO; i++) { if (md->undo[i] && strcmp(from, md->undo[i]) == 0) { g_free(md->undo[i]); md->undo[i] = g_strdup(to); break; } } for (i = 0; i < MASTODON_MAX_UNDO; i++) { if (md->redo[i] && strcmp(from, md->redo[i]) == 0) { g_free(md->redo[i]); md->redo[i] = g_strdup(to); break; } } g_free(from); } /** * Show the current history. The history shows the redo * commands. */ void mastodon_history(struct im_connection *ic, gboolean undo_history) { struct mastodon_data *md = ic->proto_data; int i; for (i = 0; i < MASTODON_MAX_UNDO; i++) { // start with the last int n = (md->first_undo + i + 1) % MASTODON_MAX_UNDO; char *cmd = undo_history ? md->undo[n] : md->redo[n]; if (cmd) { gchar **cmds = g_strsplit (cmd, FS, -1); int j; for (j = 0; cmds[j]; j++) { if (n == md->current_undo) { mastodon_log(ic, "%02d > %s", MASTODON_MAX_UNDO - i, cmds[j]); } else { mastodon_log(ic, "%02d %s", MASTODON_MAX_UNDO - i, cmds[j]); } } g_strfreev(cmds); } } } /** * Commands we understand. Changes should be documented in * doc/mastodon-help.txt and on https://wiki.bitlbee.org/HowtoMastodon */ static void mastodon_handle_command(struct im_connection *ic, char *message, mastodon_undo_t undo_type) { struct mastodon_data *md = ic->proto_data; gboolean allow_post = g_ascii_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0; bee_user_t *bu = NULL; guint64 id; md->undo_type = undo_type; char *cmds = g_strdup(message); char **cmd = split_command_parts(cmds, 2); if (cmd[0] == NULL) { /* Nothing to do */ } else if (!set_getbool(&ic->acc->set, "commands") && allow_post) { /* Not supporting commands if "commands" is set to true/strict. */ } else if (g_ascii_strcasecmp(cmd[0], "help") == 0) { /* For unsupported undo and redo commands. */ mastodon_log(ic, "Please use help mastodon in the control channel, &bitlbee."); } else if (g_ascii_strcasecmp(cmd[0], "info") == 0) { if (!cmd[1]) { mastodon_log(ic, "Usage:\n" "- info instance\n" "- info [id|screenname]\n" "- info user [nick|account]\n" "- info relation [nick|account]\n" "- info [get|put|post|delete] url [args]"); } else if (g_ascii_strcasecmp(cmd[1], "instance") == 0) { mastodon_instance(ic); } else if (g_ascii_strcasecmp(cmd[1], "user") == 0) { if (cmd[2]) { mastodon_user(ic, cmd[2]); } else { mastodon_log(ic, "User info about whom?"); } } else if (g_ascii_strcasecmp(cmd[1], "relation") == 0) { if (cmd[2]) { mastodon_relation_to_user(ic, cmd[2]); } else { mastodon_log(ic, "Relation with whom?"); } } else if ((id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_status(ic, id); } } else if (g_ascii_strcasecmp(cmd[0], "api") == 0) { if (!cmd[1] || !cmd[2]) { mastodon_log(ic, "Usage: api [get|put|post|delete] endpoint params...\n" "Example: api post /lists/12/accounts account_ids[] 321"); } else if ((g_ascii_strcasecmp(cmd[1], "get") == 0 || g_ascii_strcasecmp(cmd[1], "put") == 0 || g_ascii_strcasecmp(cmd[1], "post") == 0 || g_ascii_strcasecmp(cmd[1], "delete") == 0) && cmd[2]) { char *s = strstr(cmd[2], " "); if (s) { *s = '\0'; char **args = g_strsplit(s+1, " ", 0); /* find length of null-terminated vector */ int i = 0; for (; args[i]; i++); if (i % 2) { mastodon_log(ic, "Wrong number of arguments. Did you forget the URL?"); } else { mastodon_raw(ic, cmd[1], cmd[2], args, i); } g_strfreev(args); } else { mastodon_raw(ic, cmd[1], cmd[2], NULL, 0); } } else { mastodon_log(ic, "Usage: 'api [get|put|post|delete] url [name value]*"); } } else if (g_ascii_strcasecmp(cmd[0], "undo") == 0) { if (cmd[1] == NULL) { mastodon_undo(ic); } else { // because it used to take an argument mastodon_log(ic, "Undo takes no arguments."); } } else if (g_ascii_strcasecmp(cmd[0], "redo") == 0) { if (cmd[1] == NULL) { mastodon_redo(ic); } else { mastodon_log(ic, "Redo takes no arguments."); } } else if (g_ascii_strcasecmp(cmd[0], "his") == 0 || g_ascii_strcasecmp(cmd[0], "history") == 0) { if (cmd[1] && g_ascii_strcasecmp(cmd[1], "undo") == 0) { mastodon_history(ic, TRUE); } else if (cmd[1] == NULL) { mastodon_history(ic, FALSE); } else { mastodon_log(ic, "History only takes the optional undo argument."); } } else if (g_ascii_strcasecmp(cmd[0], "del") == 0 || g_ascii_strcasecmp(cmd[0], "delete") == 0) { if (cmd[1] == NULL && md->last_id) { mastodon_status_delete(ic, md->last_id); } else if (cmd[1] && (id = mastodon_message_id_from_command_arg(ic, cmd[1], NULL, NULL, NULL, NULL))) { mastodon_status_delete(ic, id); } else { mastodon_log(ic, "Could not delete the last post."); } } else if ((g_ascii_strcasecmp(cmd[0], "favourite") == 0 || g_ascii_strcasecmp(cmd[0], "favorite") == 0 || g_ascii_strcasecmp(cmd[0], "fav") == 0 || g_ascii_strcasecmp(cmd[0], "like") == 0)) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_FAVOURITE_URL, MC_FAVOURITE, id); } else { mastodon_log(ic, "Huh? Please provide a log number or nick."); } } else if ((g_ascii_strcasecmp(cmd[0], "unfavourite") == 0 || g_ascii_strcasecmp(cmd[0], "unfavorite") == 0 || g_ascii_strcasecmp(cmd[0], "unfav") == 0 || g_ascii_strcasecmp(cmd[0], "unlike") == 0 || g_ascii_strcasecmp(cmd[0], "dislike") == 0)) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_UNFAVOURITE_URL, MC_UNFAVOURITE, id); } else { mastodon_log(ic, "What? Please provide a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "pin") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_PIN_URL, MC_PIN, id); } else { mastodon_log(ic, "Sorry, what? Please provide a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "unpin") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_UNPIN_URL, MC_UNPIN, id); } else { mastodon_log(ic, "No can do! I need a a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "follow") == 0) { if (cmd[1]) { mastodon_add_buddy(ic, cmd[1], NULL); } else { mastodon_log(ic, "I'm confused! Follow whom?"); } } else if (g_ascii_strcasecmp(cmd[0], "unfollow") == 0) { if (cmd[1]) { mastodon_remove_buddy(ic, cmd[1], NULL); } else { mastodon_log(ic, "Unfollow whom?"); } } else if (g_ascii_strcasecmp(cmd[0], "block") == 0) { if (cmd[1]) { mastodon_add_deny(ic, cmd[1]); } else { mastodon_log(ic, "Whom should I block?"); } } else if (g_ascii_strcasecmp(cmd[0], "unblock") == 0 || g_ascii_strcasecmp(cmd[0], "allow") == 0) { if (cmd[1]) { mastodon_rem_deny(ic, cmd[1]); } else { mastodon_log(ic, "Unblock who?"); } } else if (g_ascii_strcasecmp(cmd[0], "mute") == 0 && g_ascii_strcasecmp(cmd[1], "user") == 0) { if (cmd[2] && (id = mastodon_user_id_or_warn(ic, cmd[2]))) { mastodon_post(ic, MASTODON_ACCOUNT_MUTE_URL, MC_ACCOUNT_MUTE, id); } else { mastodon_log(ic, "Mute user? I also need a nick!"); } } else if (g_ascii_strcasecmp(cmd[0], "unmute") == 0 && g_ascii_strcasecmp(cmd[1], "user") == 0) { if (cmd[2] && (id = mastodon_user_id_or_warn(ic, cmd[2]))) { mastodon_post(ic, MASTODON_ACCOUNT_UNMUTE_URL, MC_ACCOUNT_UNMUTE, id); } else { mastodon_log(ic, "Sure, unmute a user. But who is it? Give me a nick!"); } } else if (g_ascii_strcasecmp(cmd[0], "mute") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_MUTE_URL, MC_STATUS_MUTE, id); } else { mastodon_log(ic, "Muting? Please provide a log number or nick!"); } } else if (g_ascii_strcasecmp(cmd[0], "unmute") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_UNMUTE_URL, MC_STATUS_UNMUTE, id); } else { mastodon_log(ic, "OK, I'll unmute something. But what? I need a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "boost") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_BOOST_URL, MC_BOOST, id); } else { mastodon_log(ic, "Failed to boost! Please provide a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "unboost") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_post(ic, MASTODON_STATUS_UNBOOST_URL, MC_UNBOOST, id); } else { mastodon_log(ic, "Argh, #fail! Please provide a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "url") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_status_show_url(ic, id); } else { mastodon_log(ic, "This is confusing. Do you have a log number or nick?"); } } else if ((g_ascii_strcasecmp(cmd[0], "whois") == 0 || g_ascii_strcasecmp(cmd[0], "who") == 0)) { if (!cmd[1]) { mastodon_log(ic, "The IRC command /names should give you a list."); } else if ((bu = mastodon_user_by_nick(ic, cmd[1]))) { mastodon_log(ic, "%s [%s]", bu->handle, bu->fullname); } else if ((parse_int64(cmd[1], 16, &id) && id < MASTODON_LOG_LENGTH)) { mastodon_show_mentions(ic, md->log[id].mentions); } else if ((parse_int64(cmd[1], 10, &id))) { mastodon_status_show_mentions(ic, id); } else if (g_ascii_strcasecmp(cmd[1], md->user) == 0) { mastodon_log(ic, "This is you!"); } else { mastodon_unknown_user_warning(ic, cmd[1]); } } else if (g_ascii_strcasecmp(cmd[0], "report") == 0 || g_ascii_strcasecmp(cmd[0], "spam") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { if (!cmd[2] || strlen(cmd[2]) == 0) { mastodon_log(ic, "You must provide a comment with your report."); } else { mastodon_report(ic, id, cmd[2]); } } else { mastodon_log(ic, "I need a log number or nick, and a comment!"); } } else if (g_ascii_strcasecmp(cmd[0], "search") == 0) { if (cmd[1]) { mastodon_search(ic, cmd[1]); } else { mastodon_log(ic, "Sure, but what?"); } } else if (g_ascii_strcasecmp(cmd[0], "context") == 0) { if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) { mastodon_context(ic, id); } else { mastodon_log(ic, "Context of what, though? Please provide a log number or nick."); } } else if (g_ascii_strcasecmp(cmd[0], "timeline") == 0) { if (!cmd[1] || strcmp(cmd[1], "home") == 0) { mastodon_home_timeline(ic); } else if ((bu = mastodon_user_by_nick(ic, cmd[1])) && (id = mastodon_account_id(bu))) { mastodon_account_statuses(ic, id); } else if (*cmd[1] == '#') { mastodon_hashtag_timeline(ic, cmd[1] + 1); } else if (*cmd[1] == '@') { mastodon_unknown_account_statuses(ic, cmd[1] + 1); } else if (strcmp(cmd[1], "local") == 0) { mastodon_local_timeline(ic); } else if (strcmp(cmd[1], "federated") == 0) { mastodon_federated_timeline(ic); } else { mastodon_unknown_list_timeline(ic, message + 9); // "timeline %s" } } else if (g_ascii_strcasecmp(cmd[0], "notifications") == 0) { if (cmd[1] == NULL) { mastodon_notifications(ic); } else { mastodon_log(ic, "Notifications takes no arguments."); } } else if (g_ascii_strcasecmp(cmd[0], "pinned") == 0) { if (!cmd[1]) { mastodon_log(ic, "Pin the void? I need a nick or an account."); } else if ((bu = mastodon_user_by_nick(ic, cmd[1])) && (id = mastodon_account_id(bu))) { mastodon_account_pinned_statuses(ic, id); } else { mastodon_unknown_account_pinned_statuses(ic, cmd[1]); } } else if (g_ascii_strcasecmp(cmd[0], "bio") == 0) { if (!cmd[1]) { mastodon_log(ic, "Bio what? Please provide a nick or an account."); } else if ((bu = mastodon_user_by_nick(ic, cmd[1])) && (id = mastodon_account_id(bu))) { mastodon_account_bio(ic, id); } else { mastodon_unknown_account_bio(ic, cmd[1]); } } else if (g_ascii_strcasecmp(cmd[0], "more") == 0) { if (cmd[1]) { mastodon_log(ic, "More takes no arguments."); } else if (md->next_url) { mastodon_more(ic); } else { mastodon_log(ic, "More of what? Use the timeline command, first."); } } else if (g_ascii_strcasecmp(cmd[0], "list") == 0) { if (!cmd[1]) { mastodon_lists(ic); } else if (g_ascii_strcasecmp(cmd[1], "create") == 0) { if (!cmd[2]) { mastodon_log(ic, "You forgot the title of the new list!"); } else { mastodon_list_create(ic, message + 12); // "list create %s" } } else if (g_ascii_strcasecmp(cmd[1], "reload") == 0) { if (cmd[2]) { mastodon_log(ic, "List reloading takes no argument"); } else { mastodon_list_reload(ic, FALSE); } } else if (g_ascii_strcasecmp(cmd[1], "delete") == 0) { if (!cmd[2]) { mastodon_log(ic, "Which list should be deleted? Use list to find out."); } else { mastodon_unknown_list_delete(ic, message + 12); // "list delete %s" } } else if (g_ascii_strcasecmp(cmd[1], "add") == 0) { char **args = g_strsplit(cmd[2], " to ", 2); if (args[0] && args[1] && (id = mastodon_user_id_or_warn(ic, args[0]))) { mastodon_unknown_list_add_account(ic, id, args[1]); } else { mastodon_log(ic, "I am confused. Please use list add <nick> to <list>."); } g_strfreev(args); } else if (g_ascii_strcasecmp(cmd[1], "remove") == 0) { char **args = g_strsplit(cmd[2], " from ", 2); if (args[0] && args[1] && (id = mastodon_user_id_or_warn(ic, args[0]))) { mastodon_unknown_list_remove_account(ic, id, args[1]); } else { mastodon_log(ic, "I need to what to do! Use list remove <nick> from <list>."); } g_strfreev(args); } else { mastodon_unknown_list_accounts(ic, message + 5); // "list %s" } } else if (g_ascii_strcasecmp(cmd[0], "filter") == 0) { if (!cmd[1]) { mastodon_filters(ic); } else if (g_ascii_strcasecmp(cmd[1], "create") == 0) { if (!cmd[2]) { mastodon_log(ic, "What do you want to filter?"); } else { mastodon_filter_create(ic, message + 14); // "filter create %s" } } else if (g_ascii_strcasecmp(cmd[1], "delete") == 0) { if (!cmd[2]) { mastodon_log(ic, "Which filter should be deleted? Use filter to find out."); } else { mastodon_filter_delete(ic, cmd[2]); } } else { mastodon_log(ic, "I only understand the filter subcommands create and delete."); } } else if (g_ascii_strcasecmp(cmd[0], "reply") == 0) { if (!cmd[1] || !cmd[2]) { mastodon_log(ic, "Sorry, what? Please provide a log number or nick, and your reply."); } else { /* These three variables will be set, if we find the toot we are replying to in our log or in the * mastodon_user_data (mud). If we are replying to a fixed id, then we'll get an id and the three variables * remain untouched, so handle them with care. */ GSList *mentions = NULL; char *spoiler_text = NULL; mastodon_visibility_t visibility = MV_UNKNOWN; if ((id = mastodon_message_id_or_warn_and_more(ic, cmd[1], &bu, &mentions, &visibility, &spoiler_text))) { mastodon_visibility_t default_visibility = mastodon_default_visibility(ic); if (default_visibility > visibility) visibility = default_visibility; char *who = bu ? bu->handle : NULL; mastodon_post_message(ic, cmd[2], id, who, MASTODON_REPLY, mentions, visibility, spoiler_text); } else { mastodon_log(ic, "Sorry, I can't figure out what you're reply to!"); } } } else if (g_ascii_strcasecmp(cmd[0], "cw") == 0) { g_free(md->spoiler_text); if (cmd[1] == NULL) { md->spoiler_text = NULL; mastodon_log(ic, "Next post will get no content warning"); } else { md->spoiler_text = g_strdup(message + 3); mastodon_log(ic, "Next post will get content warning '%s'", md->spoiler_text); } } else if ((g_ascii_strcasecmp(cmd[0], "visibility") == 0 || g_ascii_strcasecmp(cmd[0], "vis") == 0)) { if (cmd[1] == NULL) { md->visibility = mastodon_default_visibility(ic); } else { md->visibility = mastodon_parse_visibility(cmd[1]); } mastodon_log(ic, "Next post is %s", mastodon_visibility(md->visibility)); } else if (g_ascii_strcasecmp(cmd[0], "post") == 0) { if (cmd[1] == NULL) { mastodon_log(ic, "What should we post?"); } else { mastodon_post_message(ic, message + 5, 0, cmd[1], MASTODON_NEW_MESSAGE, NULL, MV_UNKNOWN, NULL); } } else if (g_ascii_strcasecmp(cmd[0], "public") == 0 || g_ascii_strcasecmp(cmd[0], "unlisted") == 0 || g_ascii_strcasecmp(cmd[0], "private") == 0 || g_ascii_strcasecmp(cmd[0], "direct") == 0) { mastodon_log(ic, "Please use the visibility command instead"); } else if (allow_post) { mastodon_post_message(ic, message, 0, cmd[0], MASTODON_MAYBE_REPLY, NULL, MV_UNKNOWN, NULL); } else { mastodon_log(ic, "Unknown command: %s", cmd[0]); } g_free(cmds); } void mastodon_log(struct im_connection *ic, char *format, ...) { struct mastodon_data *md = ic->proto_data; va_list params; char *text; va_start(params, format); text = g_strdup_vprintf(format, params); va_end(params); if (md->timeline_gc) { imcb_chat_log(md->timeline_gc, "%s", text); } else { imcb_log(ic, "%s", text); } g_free(text); } G_MODULE_EXPORT void init_plugin(void) { struct prpl *ret = g_new0(struct prpl, 1); ret->options = PRPL_OPT_NOOTR | PRPL_OPT_NO_PASSWORD; ret->name = "mastodon"; ret->login = mastodon_login; ret->init = mastodon_init; ret->logout = mastodon_logout; ret->buddy_msg = mastodon_buddy_msg; ret->get_info = mastodon_get_info; ret->add_buddy = mastodon_add_buddy; ret->remove_buddy = mastodon_remove_buddy; ret->chat_msg = mastodon_chat_msg; ret->chat_join = mastodon_chat_join; ret->chat_leave = mastodon_chat_leave; ret->add_permit = mastodon_add_permit; ret->rem_permit = mastodon_rem_permit; ret->add_deny = mastodon_add_deny; ret->rem_deny = mastodon_rem_deny; ret->buddy_data_add = mastodon_buddy_data_add; ret->buddy_data_free = mastodon_buddy_data_free; ret->handle_cmp = g_ascii_strcasecmp; register_protocol(ret); } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/mastodon.h���������������������������������������������������������������0000664�0000000�0000000�00000015656�13432067562�0017556�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/***************************************************************************\ * * * BitlBee - An IRC to IM gateway * * Simple module to facilitate Mastodon functionality. * * * * Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> * * Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net> * * Copyright 2017-2018 Alex Schroeder <alex@gnu.org> * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation, version * * 2.1. * * * * This library 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 * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public License * * along with this library; if not, write to the Free Software Foundation, * * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * * ****************************************************************************/ #include "nogaim.h" #pragma once #ifdef DEBUG_MASTODON #define debug(text ...) imcb_log(ic, text); #else #define debug(text ...) #endif #define MASTODON_OAUTH_HANDLE "mastodon_oauth" #define MASTODON_SCOPE "read%20write%20follow" // URL escaped #define MASTODON_URL_REGEX "https?://\\S+" #define MASTODON_MENTION_REGEX "@(([a-zA-Z0-9_]+)@[a-zA-Z0-9.-]+[a-zA-Z0-9])" typedef enum { MASTODON_HAVE_FRIENDS = 0x00001, MASTODON_MODE_ONE = 0x00002, MASTODON_MODE_MANY = 0x00004, MASTODON_MODE_CHAT = 0x00008, MASTODON_GOT_TIMELINE = 0x00010, MASTODON_GOT_NOTIFICATIONS = 0x00020, MASTODON_GOT_FILTERS = 0x00040, MASTODON_GOT_STATUS = 0x00100, MASTODON_GOT_CONTEXT = 0x00200, } mastodon_flags_t; typedef enum { MASTODON_DIRECT, MASTODON_REPLY, MASTODON_MAYBE_REPLY, MASTODON_NEW_MESSAGE, } mastodon_message_t; /** * Visibility of a status. MV_UNKNOWN mean that we will use the default visibility when posting. * Higher has precedence! */ typedef enum { MV_UNKNOWN, MV_PUBLIC, MV_UNLISTED, MV_PRIVATE, MV_DIRECT, } mastodon_visibility_t; /** * Various things that come in pages such that the "more" command needs to know about it. */ typedef enum { MASTODON_MORE_STATUSES, MASTODON_MORE_NOTIFICATIONS, } mastodon_more_t; /** * These are the various ways a command can influence the undo/redo * queue. */ typedef enum { MASTODON_NEW, MASTODON_UNDO, MASTODON_REDO, } mastodon_undo_t; #define FS "\x1e" /** * These are the commands that can be undone and redone. */ typedef enum { MC_UNKNOWN, MC_POST, MC_DELETE, MC_FOLLOW, MC_UNFOLLOW, MC_BLOCK, MC_UNBLOCK, MC_FAVOURITE, MC_UNFAVOURITE, MC_PIN, MC_UNPIN, MC_ACCOUNT_MUTE, MC_ACCOUNT_UNMUTE, MC_STATUS_MUTE, MC_STATUS_UNMUTE, MC_BOOST, MC_UNBOOST, MC_LIST_CREATE, MC_LIST_DELETE, MC_LIST_ADD_ACCOUNT, MC_LIST_REMOVE_ACCOUNT, MC_FILTER_CREATE, MC_FILTER_DELETE, } mastodon_command_type_t; struct mastodon_log_data; #define MASTODON_MAX_UNDO 10 struct mastodon_data { char* user; /* to be used when parsing commands */ struct oauth2_service *oauth2_service; char *oauth2_access_token; gpointer home_timeline_obj; /* of mastodon_list */ gpointer notifications_obj; /* of mastodon_list */ gpointer status_obj; /* of mastodon_status */ gpointer context_before_obj; /* of mastodon_list */ gpointer context_after_obj; /* of mastodon_list */ GSList *streams; /* of struct http_request */ struct groupchat *timeline_gc; guint64 seen_id; /* For deduplication */ mastodon_flags_t flags; GSList *filters; /* of struct mastodon_filter */ guint64 last_id; /* Information about our last status posted */ mastodon_visibility_t last_visibility; char *last_spoiler_text; GSList *mentions; mastodon_visibility_t visibility; /* visibility for the next status */ char *spoiler_text; /* CW for the next post */ mastodon_undo_t undo_type; /* for the current command */ char *undo[MASTODON_MAX_UNDO]; /* a small stack of undo statements */ char *redo[MASTODON_MAX_UNDO]; /* a small stack of redo statements */ int first_undo; /* index of the latest item in the undo and redo stacks */ int current_undo; /* index of the current item in the undo and redo stacks */ /* for the more command */ char *next_url; mastodon_more_t more_type; /* set base_url */ gboolean url_ssl; int url_port; char *url_host; char *url_path; char *name; /* Used to generate contact + channel name. */ /* set show_ids */ struct mastodon_log_data *log; int log_id; }; struct mastodon_user_data { guint64 account_id; guint64 last_id; /* last status id (in case we reply to it) */ time_t last_time; /* when was this last status sent (if we maybe reply) */ mastodon_visibility_t visibility; /* what visibility did it have so can use it in our reply */ GSList *mentions; /* what accounts did it mention so we can mention them in our reply, too */ char *spoiler_text; /* what CW did it use so we can keep it in our reply */ GSList *lists; /* list membership of this account */ }; #define MASTODON_LOG_LENGTH 256 struct mastodon_log_data { guint64 id; /* DANGER: bu can be a dead pointer. Check it first. * mastodon_message_id_from_command_arg() will do this. */ struct bee_user *bu; mastodon_visibility_t visibility; GSList *mentions; char *spoiler_text; }; /** * This has the same function as the msn_connections GSList. We use this to * make sure the connection is still alive in callbacks before we do anything * else. */ extern GSList *mastodon_connections; /** * Evil hack: Fake bee_user which will always point at the local user. * Sometimes used as a return value by mastodon_message_id_from_command_arg. * NOT thread safe but don't you dare to even think of ever making BitlBee * threaded. :-) */ extern bee_user_t mastodon_log_local_user; struct http_request; char *mastodon_parse_error(struct http_request *req); void mastodon_log(struct im_connection *ic, char *format, ...); void oauth2_init(struct im_connection *ic); struct groupchat *mastodon_groupchat_init(struct im_connection *ic); void mastodon_do(struct im_connection *ic, char *redo, char *undo); void mastodon_do_update(struct im_connection *ic, char *to); ����������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/rot13.c������������������������������������������������������������������0000664�0000000�0000000�00000000334�13432067562�0016660�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������void rot13(char *s) { for (; *s != 0; s++) { if ((*s >= 'A' && *s <= 'M') || (*s >= 'a' && *s <= 'm')) { *s = *s + 13; } else if ((*s >= 'N' && *s <= 'Z') || (*s >= 'n' && *s <= 'z')) { *s = *s - 13; } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bitlbee-mastodon-1.4.1/src/rot13.h������������������������������������������������������������������0000664�0000000�0000000�00000000025�13432067562�0016662�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������void rot13(char *s); �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������