mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 13:39:27 -07:00
Compare commits
1 Commits
4d54ea03e8
...
auto-fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2226071d2 |
@@ -1,7 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
use fb_utils::{determine_format, get_var_screeninfo};
|
||||
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
|
||||
use std::io::Cursor;
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::time::Duration;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
@@ -20,6 +24,20 @@ pub struct Dimensions {
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum FbFormat {
|
||||
ARGB888,
|
||||
ABGR888,
|
||||
RGB888,
|
||||
BGR888,
|
||||
RGB666,
|
||||
RGB565,
|
||||
BGR565,
|
||||
RGB555,
|
||||
BGR555,
|
||||
RGB444,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Color {
|
||||
@@ -131,6 +149,109 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to determine the FB dimensions from FB vinfo.
|
||||
pub fn read_fb_dimentions(fb: BorrowedFd<'_>) -> std::io::Result<Dimensions> {
|
||||
let vinfo = get_var_screeninfo(fb)?;
|
||||
Ok(Dimensions {
|
||||
height: vinfo.yres,
|
||||
width: vinfo.xres,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to determine the FBs format
|
||||
///
|
||||
/// Returns `Ok(None)` if the format cannot be determined
|
||||
pub fn read_fb_format(fb: BorrowedFd<'_>) -> std::io::Result<Option<FbFormat>> {
|
||||
let vinfo = get_var_screeninfo(fb)?;
|
||||
Ok(determine_format(vinfo))
|
||||
}
|
||||
|
||||
pub fn buffer_to_fb_format(
|
||||
buffer: &Vec<(u8, u8, u8)>,
|
||||
format: &FbFormat,
|
||||
big_endian: bool,
|
||||
) -> Vec<u8> {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
match format {
|
||||
FbFormat::RGB565 => {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
if big_endian {
|
||||
raw_buffer.extend(rgb565.to_be_bytes());
|
||||
} else {
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
}
|
||||
other => panic!("This display uses a format we haven't implemneted yet {other:?}"),
|
||||
}
|
||||
}
|
||||
raw_buffer
|
||||
}
|
||||
|
||||
pub type CallBack = Box<dyn FnMut(&mut FbInner, &[(u8, u8, u8)]) + Send + 'static>;
|
||||
|
||||
pub struct FramebufferDevice {
|
||||
data: FbInner,
|
||||
pre_write_fn: Option<CallBack>,
|
||||
post_write_fn: Option<CallBack>,
|
||||
}
|
||||
|
||||
pub struct FbInner {
|
||||
pub fd: File,
|
||||
pub dims: Dimensions,
|
||||
pub format: FbFormat,
|
||||
}
|
||||
|
||||
impl FramebufferDevice {
|
||||
pub fn new(
|
||||
path: &str,
|
||||
pre_write_fn: Option<CallBack>,
|
||||
post_write_fn: Option<CallBack>,
|
||||
) -> Self {
|
||||
// This is done as a blocking call to prevent all of the UI init code from having to
|
||||
// be made async, making it more verbose. This is a single syscall that would have been
|
||||
// done via spawn_blocking anyway, and it's done once on startup.
|
||||
let fb = std::fs::File::create(path).expect("Failed to open /dev/fb0");
|
||||
let dims = read_fb_dimentions(fb.as_fd()).expect("Failed to read FB dimensions");
|
||||
let format = read_fb_format(fb.as_fd())
|
||||
.expect("Failed to read FB format")
|
||||
.expect("FB retruned unexpected format");
|
||||
Self {
|
||||
data: FbInner {
|
||||
fd: File::from_std(fb),
|
||||
dims,
|
||||
format,
|
||||
},
|
||||
pre_write_fn,
|
||||
post_write_fn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for FramebufferDevice {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
self.data.dims
|
||||
}
|
||||
|
||||
async fn write_buffer(
|
||||
&mut self,
|
||||
buffer: Vec<(u8, u8, u8)>, // rgb, row-wise, left-to-right, top-to-bottom
|
||||
) {
|
||||
if let Some(func) = self.pre_write_fn.as_mut() {
|
||||
func(&mut self.data, &buffer);
|
||||
}
|
||||
let raw_buffer = buffer_to_fb_format(&buffer, &self.data.format, false);
|
||||
self.data.fd.write_all(&raw_buffer).await.unwrap();
|
||||
self.data.fd.rewind().await.unwrap();
|
||||
if let Some(func) = self.post_write_fn.as_mut() {
|
||||
func(&mut self.data, &buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
@@ -201,3 +322,184 @@ pub fn update_ui(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mod fb_utils {
|
||||
use std::io::{Error, Result};
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
|
||||
use libc::ioctl;
|
||||
|
||||
use super::FbFormat;
|
||||
|
||||
const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
|
||||
// const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
|
||||
|
||||
/// Bitfield which is a part of VarScreeninfo.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Bitfield {
|
||||
pub offset: u32,
|
||||
pub length: u32,
|
||||
pub msb_right: u32,
|
||||
}
|
||||
|
||||
/// Struct as defined in /usr/include/linux/fb.h
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct VarScreeninfo {
|
||||
pub xres: u32,
|
||||
pub yres: u32,
|
||||
pub xres_virtual: u32,
|
||||
pub yres_virtual: u32,
|
||||
pub xoffset: u32,
|
||||
pub yoffset: u32,
|
||||
pub bits_per_pixel: u32,
|
||||
pub grayscale: u32,
|
||||
pub red: Bitfield,
|
||||
pub green: Bitfield,
|
||||
pub blue: Bitfield,
|
||||
pub transp: Bitfield,
|
||||
pub nonstd: u32,
|
||||
pub activate: u32,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
pub accel_flags: u32,
|
||||
pub pixclock: u32,
|
||||
pub left_margin: u32,
|
||||
pub right_margin: u32,
|
||||
pub upper_margin: u32,
|
||||
pub lower_margin: u32,
|
||||
pub hsync_len: u32,
|
||||
pub vsync_len: u32,
|
||||
pub sync: u32,
|
||||
pub vmode: u32,
|
||||
pub rotate: u32,
|
||||
pub colorspace: u32,
|
||||
pub reserved: [u32; 4],
|
||||
}
|
||||
|
||||
// /// Struct as defined in /usr/include/linux/fb.h
|
||||
// /// Note: type is a keyword in Rust and therefore has been changed to fb_type.
|
||||
// #[repr(C)]
|
||||
// #[derive(Clone, Debug, Default)]
|
||||
// pub struct FixScreeninfo {
|
||||
// pub id: [u8; 16],
|
||||
// pub smem_start: usize,
|
||||
// pub smem_len: u32,
|
||||
// pub fb_type: u32,
|
||||
// pub type_aux: u32,
|
||||
// pub visual: u32,
|
||||
// pub xpanstep: u16,
|
||||
// pub ypanstep: u16,
|
||||
// pub ywrapstep: u16,
|
||||
// pub line_length: u32,
|
||||
// pub mmio_start: usize,
|
||||
// pub mmio_len: u32,
|
||||
// pub accel: u32,
|
||||
// pub capabilities: u16,
|
||||
// pub reserved: [u16; 2],
|
||||
// }
|
||||
|
||||
// pub fn get_fix_screeninfo(fb: BorrowedFd<'_>) -> Result<FixScreeninfo> {
|
||||
// let mut info: FixScreeninfo = Default::default();
|
||||
// let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_FSCREENINFO as _, &mut info) };
|
||||
// match result {
|
||||
// -1 => Err(Error::last_os_error()),
|
||||
// _ => Ok(info),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn get_var_screeninfo(fb: BorrowedFd<'_>) -> Result<VarScreeninfo> {
|
||||
let mut info: VarScreeninfo = Default::default();
|
||||
let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_VSCREENINFO as _, &mut info) };
|
||||
match result {
|
||||
-1 => Err(Error::last_os_error()),
|
||||
_ => Ok(info),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
struct RgbaBitfield {
|
||||
red: Bitfield,
|
||||
green: Bitfield,
|
||||
blue: Bitfield,
|
||||
transp: Bitfield,
|
||||
}
|
||||
|
||||
impl From<&VarScreeninfo> for RgbaBitfield {
|
||||
fn from(value: &VarScreeninfo) -> Self {
|
||||
Self {
|
||||
red: value.red.clone(),
|
||||
green: value.green.clone(),
|
||||
blue: value.blue.clone(),
|
||||
transp: value.transp.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BitfieldShort = (u32, u32);
|
||||
type FbInfoShort = (BitfieldShort, BitfieldShort, BitfieldShort, BitfieldShort);
|
||||
|
||||
const fn tuple_to_bitfield(v: BitfieldShort) -> Bitfield {
|
||||
let (offset, length) = v;
|
||||
// None of formats we support have msb_right set.
|
||||
Bitfield {
|
||||
offset,
|
||||
length,
|
||||
msb_right: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a tuple of 4 tuples `(r, g, b, a)`. Each color tuple is a tuple of `(offset, length)`.
|
||||
const fn rgba_bitfield(v: FbInfoShort) -> RgbaBitfield {
|
||||
let (r, g, b, a) = v;
|
||||
RgbaBitfield {
|
||||
red: tuple_to_bitfield(r),
|
||||
green: tuple_to_bitfield(g),
|
||||
blue: tuple_to_bitfield(b),
|
||||
transp: tuple_to_bitfield(a),
|
||||
}
|
||||
}
|
||||
|
||||
// Logic borrowed from QT https://github.com/qt/qtbase/blob/498ae026e98ed181d1480fe5f6f2f1453a725e78/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp
|
||||
|
||||
const ARGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (24, 8)));
|
||||
const ABGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (24, 8)));
|
||||
const RGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (0, 0)));
|
||||
const BGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (0, 0)));
|
||||
const RGB666: RgbaBitfield = rgba_bitfield(((12, 6), (6, 6), (0, 6), (0, 0)));
|
||||
const RGB565: RgbaBitfield = rgba_bitfield(((11, 5), (5, 6), (0, 5), (0, 0)));
|
||||
const BGR565: RgbaBitfield = rgba_bitfield(((0, 5), (5, 6), (11, 5), (0, 0)));
|
||||
const RGB555: RgbaBitfield = rgba_bitfield(((10, 5), (5, 5), (0, 5), (0, 0)));
|
||||
const BGR555: RgbaBitfield = rgba_bitfield(((0, 5), (5, 5), (10, 5), (0, 0)));
|
||||
const RGB444: RgbaBitfield = rgba_bitfield(((8, 4), (4, 4), (0, 4), (0, 0)));
|
||||
|
||||
fn determine_depth(vinfo: &VarScreeninfo) -> u32 {
|
||||
let depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length;
|
||||
match vinfo.bits_per_pixel {
|
||||
24 if depth == 0 => 24,
|
||||
16 if depth == 0 => 16,
|
||||
24 | 16 => depth,
|
||||
v => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn determine_format(vinfo: VarScreeninfo) -> Option<FbFormat> {
|
||||
let rgba = RgbaBitfield::from(&vinfo);
|
||||
let depth = determine_depth(&vinfo);
|
||||
|
||||
match (depth, rgba) {
|
||||
(32, ARGB888) => Some(FbFormat::ARGB888),
|
||||
(32, ABGR888) => Some(FbFormat::ABGR888),
|
||||
(24, RGB888) => Some(FbFormat::RGB888),
|
||||
(24, BGR888) => Some(FbFormat::BGR888),
|
||||
(18, RGB666) => Some(FbFormat::RGB666),
|
||||
(16, RGB565) => Some(FbFormat::RGB565),
|
||||
(16, BGR565) => Some(FbFormat::BGR565),
|
||||
(15, RGB555) => Some(FbFormat::RGB555),
|
||||
(15, BGR555) => Some(FbFormat::BGR555),
|
||||
(12, RGB444) => Some(FbFormat::RGB444),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use async_trait::async_trait;
|
||||
use crate::display::generic_framebuffer;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use super::generic_framebuffer::FramebufferDevice;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
// TODO actually poll for this, maybe w/ fbset?
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 128,
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
@@ -44,7 +19,7 @@ pub fn update_ui(
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
FramebufferDevice::new(FB_PATH, None, None),
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use async_trait::async_trait;
|
||||
use std::os::fd::AsRawFd;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use crate::display::generic_framebuffer;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
use super::generic_framebuffer::{FbInner, FramebufferDevice};
|
||||
|
||||
struct Framebuffer;
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[repr(C)]
|
||||
struct fb_fillrect {
|
||||
@@ -25,54 +22,28 @@ struct fb_fillrect {
|
||||
rop: u32,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
// TODO actually poll for this, maybe w/ fbset?
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 128,
|
||||
}
|
||||
}
|
||||
fn update_display(fb: &mut FbInner, buffer: &[(u8, u8, u8)]) {
|
||||
let width = fb.dims.width;
|
||||
let height = buffer.len() as u32 / width;
|
||||
let mut arg = fb_fillrect {
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
width,
|
||||
height,
|
||||
color: 0xffff, // not sure what this is
|
||||
rop: 0,
|
||||
};
|
||||
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
|
||||
let dimensions = self.dimensions();
|
||||
let width = dimensions.width;
|
||||
let height = buffer.len() as u32 / width;
|
||||
let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
|
||||
let mut arg = fb_fillrect {
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
width,
|
||||
height,
|
||||
color: 0xffff, // not sure what this is
|
||||
rop: 0,
|
||||
};
|
||||
unsafe {
|
||||
let res = libc::ioctl(
|
||||
fb.fd.as_raw_fd(),
|
||||
0x4619, // FBIORECT_DISPLAY
|
||||
&mut arg as *mut _,
|
||||
std::mem::size_of::<fb_fillrect>(),
|
||||
);
|
||||
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
// note: big-endian!
|
||||
raw_buffer.extend(rgb565.to_be_bytes());
|
||||
}
|
||||
|
||||
f.write_all(&raw_buffer).await.unwrap();
|
||||
|
||||
// ioctl is a synchronous operation, but it's fast enough that it shouldn't block
|
||||
unsafe {
|
||||
let res = libc::ioctl(
|
||||
f.as_raw_fd(),
|
||||
0x4619, // FBIORECT_DISPLAY
|
||||
&mut arg as *mut _,
|
||||
std::mem::size_of::<fb_fillrect>(),
|
||||
);
|
||||
|
||||
if res < 0 {
|
||||
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
|
||||
}
|
||||
if res < 0 {
|
||||
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +57,7 @@ pub fn update_ui(
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
FramebufferDevice::new(FB_PATH, None, Some(Box::new(update_display))),
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
|
||||
@@ -1,45 +1,21 @@
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use async_trait::async_trait;
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use super::generic_framebuffer::FramebufferDevice;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 160,
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
@@ -49,7 +25,7 @@ pub fn update_ui(
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
FramebufferDevice::new(FB_PATH, None, None),
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user