First
[anni] / lib / pleroma / bbs / handler.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.BBS.Handler do
6   use Sshd.ShellHandler
7   alias Pleroma.Activity
8   alias Pleroma.HTML
9   alias Pleroma.Web.ActivityPub.ActivityPub
10   alias Pleroma.Web.CommonAPI
11
12   def on_shell(username, _pubkey, _ip, _port) do
13     :ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!")
14     user = Pleroma.User.get_cached_by_nickname(to_string(username))
15     Logger.debug("#{inspect(user)}")
16     loop(run_state(user: user))
17   end
18
19   def on_connect(username, ip, port, method) do
20     Logger.debug(fn ->
21       """
22       Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{inspect(port)} using #{inspect(method)}
23       """
24     end)
25   end
26
27   def on_disconnect(username, ip, port) do
28     Logger.debug(fn ->
29       "Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}"
30     end)
31   end
32
33   defp loop(state) do
34     self_pid = self()
35     counter = state.counter
36     prefix = state.prefix
37     user = state.user
38
39     input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end)
40     wait_input(state, input)
41   end
42
43   def puts_activity(activity) do
44     status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
45
46     IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
47
48     status.content
49     |> String.split("<br/>")
50     |> Enum.map(&HTML.strip_tags/1)
51     |> Enum.map(&HtmlEntities.decode/1)
52     |> Enum.map(&IO.puts/1)
53   end
54
55   def puts_notification(activity, user) do
56     notification =
57       Pleroma.Web.MastodonAPI.NotificationView.render("show.json", %{
58         notification: activity,
59         for: user
60       })
61
62     IO.puts(
63       "== (#{notification.type}) #{notification.status.id} by #{notification.account.display_name} (#{notification.account.acct})"
64     )
65
66     notification.status.content
67     |> String.split("<br/>")
68     |> Enum.map(&HTML.strip_tags/1)
69     |> Enum.map(&HtmlEntities.decode/1)
70     |> (fn x ->
71           case x do
72             [content] ->
73               "> " <> content
74
75             [head | _tail] ->
76               # "> " <> hd <> "..."
77               head
78               |> String.slice(1, 80)
79               |> (fn x -> "> " <> x <> "..." end).()
80           end
81         end).()
82     |> IO.puts()
83
84     IO.puts("")
85   end
86
87   def handle_command(state, "help") do
88     IO.puts("Available commands:")
89     IO.puts("help - This help")
90     IO.puts("home - Show the home timeline")
91     IO.puts("p <text> - Post the given text")
92     IO.puts("r <id> <text> - Reply to the post with the given id")
93     IO.puts("t <id> - Show a thread from the given id")
94     IO.puts("n - Show notifications")
95     IO.puts("n read - Mark all notifactions as read")
96     IO.puts("f <id> - Favourites the post with the given id")
97     IO.puts("R <id> - Repeat the post with the given id")
98     IO.puts("quit - Quit")
99
100     state
101   end
102
103   def handle_command(%{user: user} = state, "r " <> text) do
104     text = String.trim(text)
105     [activity_id, rest] = String.split(text, " ", parts: 2)
106
107     with %Activity{} <- Activity.get_by_id(activity_id),
108          {:ok, _activity} <-
109            CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
110       IO.puts("Replied!")
111     else
112       _e -> IO.puts("Could not reply...")
113     end
114
115     state
116   end
117
118   def handle_command(%{user: user} = state, "t " <> activity_id) do
119     with %Activity{} = activity <- Activity.get_by_id(activity_id) do
120       activities =
121         ActivityPub.fetch_activities_for_context(activity.data["context"], %{
122           blocking_user: user,
123           user: user,
124           exclude_id: activity.id
125         })
126
127       case activities do
128         [] ->
129           activity_id
130           |> Activity.get_by_id()
131           |> puts_activity()
132
133         _ ->
134           activities
135           |> Enum.reverse()
136           |> Enum.each(&puts_activity/1)
137       end
138     else
139       _e -> IO.puts("Could not show this thread...")
140     end
141
142     state
143   end
144
145   def handle_command(%{user: user} = state, "n read") do
146     Pleroma.Notification.clear(user)
147     IO.puts("All notifications were marked as read")
148
149     state
150   end
151
152   def handle_command(%{user: user} = state, "n") do
153     user
154     |> Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(%{})
155     |> Enum.each(&puts_notification(&1, user))
156
157     state
158   end
159
160   def handle_command(%{user: user} = state, "p " <> text) do
161     text = String.trim(text)
162
163     with {:ok, activity} <- CommonAPI.post(user, %{status: text}) do
164       IO.puts("Posted! ID: #{activity.id}")
165     else
166       _e -> IO.puts("Could not post...")
167     end
168
169     state
170   end
171
172   def handle_command(%{user: user} = state, "f " <> id) do
173     id = String.trim(id)
174
175     with %Activity{} = activity <- Activity.get_by_id(id),
176          {:ok, _activity} <- CommonAPI.favorite(user, activity) do
177       IO.puts("Favourited!")
178     else
179       _e -> IO.puts("Could not Favourite...")
180     end
181
182     state
183   end
184
185   def handle_command(state, "home") do
186     user = state.user
187
188     params =
189       %{}
190       |> Map.put(:type, ["Create"])
191       |> Map.put(:blocking_user, user)
192       |> Map.put(:muting_user, user)
193       |> Map.put(:user, user)
194
195     activities =
196       [user.ap_id | Pleroma.User.following(user)]
197       |> ActivityPub.fetch_activities(params)
198
199     Enum.each(activities, fn activity ->
200       puts_activity(activity)
201     end)
202
203     state
204   end
205
206   def handle_command(state, command) do
207     IO.puts("Unknown command '#{command}'")
208     state
209   end
210
211   defp wait_input(state, input) do
212     receive do
213       {:input, ^input, "quit\n"} ->
214         IO.puts("Exiting...")
215
216       {:input, ^input, code} when is_binary(code) ->
217         code = String.trim(code)
218
219         state = handle_command(state, code)
220
221         loop(%{state | counter: state.counter + 1})
222
223       {:input, ^input, {:error, :interrupted}} ->
224         IO.puts("Caught Ctrl+C...")
225         loop(%{state | counter: state.counter + 1})
226
227       {:input, ^input, msg} ->
228         :ok = Logger.warn("received unknown message: #{inspect(msg)}")
229         loop(%{state | counter: state.counter + 1})
230     end
231   end
232
233   defp run_state(opts) do
234     %{prefix: "pleroma", counter: 1, user: opts[:user]}
235   end
236
237   defp io_get(pid, prefix, counter, username) do
238     prompt = prompt(prefix, counter, username)
239     send(pid, {:input, self(), IO.gets(:stdio, prompt)})
240   end
241
242   defp prompt(prefix, counter, username) do
243     prompt = "#{username}@#{prefix}:#{counter}>"
244     prompt <> " "
245   end
246 end